API reference
Stepora has a small REST API that mirrors the Wati v3 API shape, so code written against Wati's v3 endpoints works against Stepora with the same paths and the same Bearer-token auth. It reads from and writes to your own Stepora account — never to Meta directly.
Base URL: https://stepora.xyz/api/ext/v3
- All requests and responses are JSON.
- List endpoints return
{ "data": [...], "pagination": { "total": 0, "limit": 50, "offset": 0 } }. - Pagination:
?limit=(default 50, maximum 200) and?offset=(default 0). - Every request made with an API key is recorded in Developers → Logs (key prefix, endpoint, method, status, latency). No request or response bodies are ever stored, and raw keys are never stored or logged.
1. Authentication — API keys
Every /api/ext/v3 call needs an API key in the Authorization header:
Authorization: Bearer sk_<your-key>
How to create one: in the panel, open Developers → API keys → New key. Give it a name and pick its scopes (below). The full key is shown once — copy it then. It is never shown again, never emailed, and never logged. If you lose it, rotate the key (below) and copy the new one.
- A client key acts for that client's own business. It cannot touch another client's data.
- An admin key must name the business on every call with
?client_id=<id>. Without it the call fails withclient_id_required_for_admin_key. - A key with an expiry date stops working after that date (
api_key_expired). - A key without the scope an endpoint needs fails with
insufficient_scopeand names the scope it needs inrequired_scope.
Rotating a key
If a key leaks, rotate it instead of deleting it: in Developers → API keys, press Rotate next to the key (or call POST /api/developer/keys/:id/rotate from the panel session). Rotation keeps the key's name, scopes, owner, and expiry, generates a brand-new key, and the old key stops working immediately. The new key is returned once — copy it then. The key list shows rotated_at, the date and time of the last rotation, and the new key's prefix.
2. Scopes
Pick only the scopes your integration needs. A key with messages:read cannot send, and a key with messages:send cannot read contacts.
messages:read— list channels, conversations, messages, and media.messages:send— send template, session, and file messages.contacts:read— list and search contacts.contacts:write— update contact attributes.templates:read— list templates.campaigns:read— read campaign data (reserved; no campaign endpoints yet).webhooks:read— read webhook data (reserved; webhooks are managed in the panel).
3. Read endpoints
These are live today.
GET /api/ext/v3
Index: API name, version, your client_id, and the exact list of covered and not-covered endpoints. Use it to check what this build supports before you code against it.
curl https://stepora.xyz/api/ext/v3 \
-H "Authorization: Bearer sk_your_key_here"
GET /api/ext/v3/channels
Your WhatsApp channels (instances). Scope: messages:read. Secrets such as access tokens are never returned.
Example response:
{
"data": [
{
"id": 1,
"name": "Main store",
"phone_number": "923001234567",
"phone_number_id": "1234567890",
"waba_id": "0987654321",
"status": "connected",
"provider": "whatsapp",
"created_at": "2026-11-06 10:00:00"
}
],
"pagination": { "total": 1 }
}
GET /api/ext/v3/conversations
Your Team Inbox conversations, newest first. Scope: messages:read. Filter with ?status=open, assigned, resolved, or closed. Any other value returns invalid_status.
curl "https://stepora.xyz/api/ext/v3/conversations?status=open&limit=20" \
-H "Authorization: Bearer sk_your_key_here"
Each conversation has id, contact_phone, contact_name, status, assigned_to, unread_count, last_message_at, and created_at.
GET /api/ext/v3/conversations/:id/messages
The messages of one conversation, oldest first. Scope: messages:read. A conversation that is not yours (or does not exist) returns conversation_not_found.
Each message has id, direction (in or out), body, type, wa_message_id, status, and created_at.
GET /api/ext/v3/templates
Your message templates. Scope: templates:read. Filter with ?status=draft, submitted, approved, or rejected, and ?category=marketing, utility, or authentication. Any other value returns invalid_status or invalid_category.
Each template has id, name, category, language, header (type and text), body, footer, buttons, status, and created_at. Only approved templates can be sent — see the send endpoint below.
GET /api/ext/v3/contacts
Your contacts. Scope: contacts:read. Search with ?search= (matches phone, name, or email) and filter with ?opt_in=0 or ?opt_in=1. Any other opt_in value returns invalid_opt_in.
curl "https://stepora.xyz/api/ext/v3/contacts?search=0300&opt_in=1&limit=20" \
-H "Authorization: Bearer sk_your_key_here"
Each contact has id, phone, name, email, tags, opt_in, lead_score, intent, source, last_seen_at, and created_at.
4. Write endpoints — added in this build
These five endpoints were added in this build, alongside this page. They are the Wati v3 write surface: send a template, send a session message, send a file, download media, and update contact attributes. Each one checks the scope, the WhatsApp rules, and your wallet before it sends — a failed check returns an error and nothing is sent or charged.
POST /api/ext/v3/messages/template
Send an approved template message — the most-used endpoint, and the only way to start a conversation outside the 24-hour window. Scope: messages:send.
- Body:
phone(the customer's phone number),template_name(the name of anapprovedtemplate),parameters(an array of values for the template's{{variables}}, in order), andbroadcast_name(the template name, in Wati's naming). - Checks: the template must exist and be
approved(template_not_approvedif not); your wallet must cover the per-message fee (insufficient_balanceif not). - Returns the created message: its
id,wa_message_id,status, andcreated_at.
curl -X POST https://stepora.xyz/api/ext/v3/messages/template \
-H "Authorization: Bearer sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"phone": "923001234567",
"template_name": "order_confirmed",
"parameters": ["Ayesha", "ORD-1042"],
"broadcast_name": "order_confirmed"
}'
POST /api/ext/v3/messages/session
Send a free-form session message. Scope: messages:send.
- Body:
phoneandmessage(the text to send). - Checks: the customer must have messaged you within the last 24 hours — Meta's session window. Outside it the call fails with
outside_session_window; send a template message instead. Your wallet must cover the per-message fee. - Returns the created message: its
id,wa_message_id,status, andcreated_at.
curl -X POST https://stepora.xyz/api/ext/v3/messages/session \
-H "Authorization: Bearer sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"phone": "923001234567", "message": "Your order is on its way — it should reach you by tomorrow evening."}'
POST /api/ext/v3/messages/file
Send a file (image, video, document, or voice note) as a WhatsApp message. Scope: messages:send.
- Body:
phone,file_url(a public URL the server can fetch the file from),filename, and an optionalcaption. - Checks: the same 24-hour session-window rule as session messages (
outside_session_window), and wallet balance. - Returns the created message: its
id,wa_message_id,status, andcreated_at.
GET /api/ext/v3/media/:id
Download a media file attached to a message. Scope: messages:read.
:idis the message id fromGET /api/ext/v3/conversations/:id/messages.- Returns the file itself (with its content type). A message with no media, or one that is not yours, returns
not_found.
PATCH /api/ext/v3/contacts/:id/attributes
Update a contact's custom attributes (for example city, language, or vip). Scope: contacts:write.
- Body:
attributes, an object of attribute names to values, for example{"attributes": {"city": "Lahore", "vip": true}}. - Only the attributes you send are changed; everything else on the contact stays as it is.
- Returns the updated contact in the same shape as
GET /api/ext/v3/contacts.
curl -X PATCH https://stepora.xyz/api/ext/v3/contacts/42/attributes \
-H "Authorization: Bearer sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"attributes": {"city": "Lahore", "vip": true}}'
5. Rate limits
There is no separate per-key rate limit on /api/ext/v3 today — no call is throttled just for being an API call. The limits that do apply are the real ones:
- Meta's 24-hour session window — free-form and file messages only work within 24 hours of the customer's last message. Outside it, use a template.
- Meta's daily messaging limit — each number has a Meta messaging tier; a send past the limit fails rather than queuing.
- Wallet balance — every sent message costs the per-message fee in Billing & wallet. A send with too little balance fails with
insufficient_balanceand nothing is sent.
If you send bursts, space template sends out and watch the status of each response — a failed send is never retried automatically.
6. Error codes
Errors are JSON: { "error": "code" }, sometimes with extra fields. The HTTP status is 400 for a bad request, 401 for a missing, wrong, or expired key, 403 for a scope or ownership problem, and 404 for a missing record.
api_key_required(401) — noAuthorization: Bearerheader was sent.invalid_api_key(401) — the key does not exist. Check for a typo, or that it was not rotated or revoked.api_key_expired(401) — the key passed its expiry date. Create or rotate a key.insufficient_scope(403) — the key lacks the scope the endpoint needs. The response names it inrequired_scope.client_id_required_for_admin_key(400) — an admin key must pass?client_id=<id>.forbidden(403) — the record belongs to another business.conversation_not_found(404) — no conversation with that id for your business.invalid_status(400) — a?status=filter value is not one of the allowed values.invalid_category(400) — a?category=filter value is notmarketing,utility, orauthentication.invalid_opt_in(400) —?opt_in=is not0or1.template_not_approved(400) — the template in a send is notapproved.outside_session_window(400) — a session or file message was sent more than 24 hours after the customer's last message. Send a template instead.insufficient_balance(402) — the wallet cannot cover the per-message fee. Top up first.not_found(404) — the record (key, contact, or media) does not exist.
7. Postman collection
Download the Postman collection — every endpoint above, ready to import.
- Import it into Postman (Import → File).
- Set the collection variables:
base_urlto your panel address with/api/ext/v3on the end (for examplehttps://stepora.xyz/api/ext/v3), andapi_keyto a key you created in Developers → API keys. - The write requests include example bodies — change the phone number and template name to your own test data before you send.
8. Webhooks
Webhooks are managed in the panel (Developers → Webhooks), not over this API. Each webhook has a URL, a set of events (message.received, message.sent, message.session_sent, message.template_sent, message.read, order.confirmed, handover.started, contact.created, campaign.sent), and a secret that signs deliveries with HMAC-SHA256. The secret is shown once on creation and once on rotation. Failed deliveries are retried up to 5 times (5 minutes, then 1 hour, 6 hours, 12 hours) and can be retried by hand from the webhook's delivery log.
message.session_sent fires when a session (plain-text, inside the 24-hour window) message is sent; message.template_sent fires when a template message is sent (campaign sends and the POST /api/ext/v3/messages/template endpoint). The older generic message.sent event is kept for backwards compatibility but no send currently emits it — subscribe to the session/template events instead.
What the API cannot do
- Create, revoke, or rotate API keys, or manage webhooks — those stay in the panel, behind your login.
- Create templates or submit them for approval — create them in the panel; the API only lists and sends approved ones.
- Create broadcasts or campaigns — use the panel.
- Fetch anything live from Meta — reads come from your own Stepora data only.
If an endpoint you need is missing, check GET /api/ext/v3 first — it lists exactly what is covered — then ask support.