Pular para o conteúdo principal

Management API — integration guide

The Management API (https://openapi.stevo.chat) is the official entry point to integrate systems with Stevo: instances, messaging, webhooks, Stevo AI, campaigns, StevoVoice, GHL and billing. This guide shows how to use it — the full list of endpoints, with a playground, is in the Management API reference.

Using Node.js/TypeScript?

The stevo-sdk SDK does all of this for you — safe retry, webhook verification and types. This guide is for integrating in another language (PHP, Python, Go, n8n, Make...) or for understanding what happens under the hood.

1. Authentication​

Create an API Key in the panel (profile menu → API Keys) and send it on every request:

curl https://openapi.stevo.chat/v1/me \
--header 'Authorization: Bearer stevo_sk_YOUR_KEY'

GET /v1/me shows the account, the scopes and the key restriction — it's the best endpoint to test your setup.

Scopes​

Each key only does what its scopes allow (403 insufficient_scope otherwise):

AreaScopes
Instancesinstances:read, instances:write
Messagesmessages:send, messages:read
Account webhookswebhooks:read, webhooks:write
Stevo AIai:read, ai:write
StevoVoicevoice:read, voice:manage
Campaignsdispatch:read, dispatch:manage
Billingbilling:read, billing:purchase
GHL / Agency / Linksghl:manage, agency:read, agency:manage, links:generate

Older keys with instances:manage and ai:manage keep working (they are synonyms of :write).

instances:write includes destructive operations

Total recreate, logout and instance deletion use the same scope. There is no separate scope for them — grant instances:write only to who needs it.

Instance-restricted key​

A key can be limited to some instances (allowed_instance_ids in /v1/me; null = all). Outside the list, the API answers 404 not_found, exactly as for another account's instance. Account operations (creating an instance, account webhooks) answer 403 key_restricted / 403 instance_restricted_key. Use one restricted key per customer whenever the integration serves a single customer.

2. Errors, correlation and rate limit​

Every error has the same shape:

{ "error": { "code": "not_ready", "message": "instance not connected", "request_id": "req_3f9c...", "retryable": false } }
  • retryable: true when repeating the same call makes sense (429, 5xx, upstream failure); false on other 4xx — fix the request before repeating.
  • X-Request-Id: every response carries this header (and the same value in error.request_id). You can send your own (up to 128 characters [A-Za-z0-9._:-]) and it's echoed back. Give it to support when reporting a problem.
  • Per-key rate limit: every response carries X-RateLimit-Limit and X-RateLimit-Remaining. When exceeded, 429 rate_limited with Retry-After (seconds).

3. Instances​

3.1. Tie the instance to your system: external_ref and metadata​

# Create in a free slot, already with your customer's reference
curl https://openapi.stevo.chat/v1/instances \
--header 'Authorization: Bearer stevo_sk_YOUR_KEY' \
--header 'Content-Type: application/json' \
--data '{ "name": "downtown-store", "external_ref": "WSP-000123", "metadata": { "plan": "pro" } }'

# Find by your reference
curl --get https://openapi.stevo.chat/v1/instances \
--header 'Authorization: Bearer stevo_sk_YOUR_KEY' \
--data-urlencode 'external_ref=WSP-000123'

# Update (metadata REPLACES the whole object; external_ref: null clears it)
curl -X PATCH https://openapi.stevo.chat/v1/instances/INSTANCE_UUID \
--header 'Authorization: Bearer stevo_sk_YOUR_KEY' \
--header 'Content-Type: application/json' \
--data '{ "metadata": { "plan": "enterprise" } }'

external_ref: up to 128 characters, unique per account (409 external_ref_conflict). metadata: up to 16 text→text pairs. Both travel in every event of the account webhooks.

3.2. Reconciliation — only what changed​

GET /v1/instances with updated_since becomes an incremental query, including deleted instances:

curl --get https://openapi.stevo.chat/v1/instances \
--header 'Authorization: Bearer stevo_sk_YOUR_KEY' \
--data-urlencode 'updated_since=2026-09-22T00:00:00Z' \
--data-urlencode 'limit=100'
{
"data": [ { "id": "...", "external_ref": "WSP-000123", "connected": true, "updated_at": "..." } ],
"deleted": [ { "id": "...", "external_ref": "WSP-000099", "metadata": null, "deleted_at": "..." } ],
"next_cursor": "eyJ1...",
"has_more": true,
"deleted_has_more": false
}

Follow next_cursor (send it back as cursor) while has_more is true. If deleted_has_more is true, repeat the call with updated_since = the last deleted_at received. Without updated_since, the response is the same as always ({ data, count }).

3.3. Connection, QR and deletion​

RouteEngineWhat it does
GET /v1/instances/{id}/connectionbothconnected, logged_in, name and number
GET /v1/instances/{id}/healthbothunified health (doesn't fail if the server is down)
GET /v1/instances/{id}/qrSM v2current QR (qr as data-URI) and pairing_code
POST /v1/instances/{id}/qr/refreshSM v2forces a new QR
POST /v1/instances/{id}/disconnectSM v2disconnects; { "logout": true } + ?confirm=true discards the session (irreversible)
DELETE /v1/instances/{id}?confirm=truebothdeletes the instance and frees the slot (irreversible, idempotent)

Without ?confirm=true on irreversible operations, the API answers 400 confirmation_required and does nothing. The deleted instance shows up in deleted during reconciliation and fires the instance.deleted event.

4. Sending messages​

POST /v1/instances/{id}/messages (scope messages:send) sends one message, on SM v2 or the Official API, without you dealing with the instance server:

curl https://openapi.stevo.chat/v1/instances/INSTANCE_UUID/messages \
--header 'Authorization: Bearer stevo_sk_YOUR_KEY' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: order-9f21' \
--data '{ "to": "5511999999999", "text": "Hello!" }'
  • Text: text. Media: media_url + media_type (image, video, audio, document), with optional caption/filename. Official API only: cloud_api with the raw Cloud API payload (e.g. template).
  • 201 response with engine, sent and message_id. Disconnected instance → 409 not_ready.
  • For volume, prefer campaigns or the instance server directly.

4.1. Idempotency-Key — never duplicate​

Send Idempotency-Key (1–200 characters, unique per instance, valid for 24h). Repeating the same request does not resend: the API returns the original response with the Idempotent-Replayed: true header.

ResponseMeaningWhat to do
409 idempotency_conflictsame key, different bodyuse another key
409 idempotency_in_progressthe same key is still being processedwait and retry
504 upstream_timeoutthe sending server didn't answer — it may have sentcheck the status before resending
502 upstream_unavailablecouldn't reach the instance server — nothing was sentsafe to retry

4.2. Message status​

# By message id
curl https://openapi.stevo.chat/v1/instances/INSTANCE_UUID/messages/MESSAGE_ID \
--header 'Authorization: Bearer stevo_sk_YOUR_KEY'

# By idempotency key — the right lookup after a timeout
curl --get https://openapi.stevo.chat/v1/instances/INSTANCE_UUID/messages \
--header 'Authorization: Bearer stevo_sk_YOUR_KEY' \
--data-urlencode 'idempotency_key=order-9f21'

States: queued → sent → delivered → read, or failed (scope messages:read). Transitions never go backwards and match the message.* webhook events. After an upstream_timeout, the status stays queued — and moves on its own to sent/delivered if the message actually went out.

5. Account webhooks​

An account webhook receives the events of all instances in a single envelope, normalized across SM v2 and the Official API, signed and with reliable delivery. It's the recommended option for new integrations (the per-instance webhook, /v1/instances/{id}/webhook, still exists, unsigned).

5.1. Create​

curl https://openapi.stevo.chat/v1/webhooks \
--header 'Authorization: Bearer stevo_sk_YOUR_KEY' \
--header 'Content-Type: application/json' \
--data '{
"url": "https://my-system.com/webhooks/stevo",
"events": ["message.received", "message.failed", "instance.connected", "instance.disconnected"],
"description": "My CRM",
"max_in_flight": 16
}'

The response carries the secret (whsec_...) — only in this response; afterwards, only secret_last4. Keep it in a vault/environment variable.

FieldRule
urlhttps:// and a public destination; redirects are not followed
eventslist of types, or ["*"] for all
max_in_flightconcurrent deliveries to this destination: 1 to 64, default 8. Raise it for endpoints that absorb volume (e.g. an agency with hundreds of instances)
activefalse pauses it; the API also deactivates it on its own after 410 Gone or 5 consecutive exhausted deliveries (disabled_reason: gone / too_many_failures)

Manage with GET/PATCH/DELETE /v1/webhooks/{id}. Limit of 10 webhooks per account. Requires a key without instance restriction.

Events: instance.created, instance.updated, instance.connecting, instance.connected, instance.disconnected, instance.auth_failed, instance.qr.updated, instance.deleted, message.received, message.sent, message.delivered, message.read, message.failed, message.edited, message.deleted, message.reaction.

5.2. What reaches your endpoint​

POST /webhooks/stevo HTTP/1.1
Content-Type: application/json
X-Stevo-Event-Id: evt_01J...
X-Stevo-Timestamp: 1790190000
X-Stevo-Attempt: 1
X-Stevo-Signature: sha256=5b1c...

{
"event_id": "evt_01J...",
"type": "message.received",
"attempt": 1,
"created_at": "2026-09-22T14:03:11.000Z",
"account_id": "ACCOUNT_UUID",
"instance": { "id": "INSTANCE_UUID", "external_ref": "WSP-000123", "metadata": { "plan": "pro" }, "engine": "smv2" },
"data": { "message_id": "3EB0...", "chat": "5511999999999", "from": "5511999999999", "from_me": false, "is_group": false, "type": "text", "text": "Hello!" }
}
  • Answer 2xx within 10 seconds (process in a queue if it takes longer). Without a 2xx, Stevo redelivers: up to 8 attempts over about 21h.
  • Delivery is "at least once": a redelivery arrives with the same event_id and a higher attempt. Deduplicate by event_id.
  • Answering 410 Gone deactivates the webhook.

5.3. Verify the signature​

X-Stevo-Signature = sha256= + HMAC-SHA256(secret, timestamp + "." + rawBody) in hexadecimal, where secret is the full whsec_... value and timestamp is X-Stevo-Timestamp. Also reject timestamps more than 5 minutes off (replay protection).

import hmac, hashlib, time

def valid_signature(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature.removeprefix("sha256="), expected)
function valid_signature(string $rawBody, string $signature, string $timestamp, string $secret): bool {
if (abs(time() - (int) $timestamp) > 300) return false;
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, preg_replace('/^sha256=/', '', $signature));
}
Use the RAW body

Compute the HMAC over the exact bytes received, before any JSON parsing. Re-serializing the JSON changes the body and the signature won't match.

5.4. Secret rotation​

POST /v1/webhooks/{id}/rotate-secret generates a new secret (returned once). During the overlap_seconds window (0–86400, default 86400 = 24h), each delivery also carries X-Stevo-Signature-Previous, signed with the previous secret — accept either one while you swap the configuration. overlap_seconds: 0 is the emergency rotation (the old one stops working immediately).

5.5. Delivery history and redelivery​

RouteWhat it does
GET /v1/webhooks/deliverieslists deliveries; filters webhook_id, instance_id, status (pending, delivered, failed, exhausted), event_type, since, cursor, limit
GET /v1/webhooks/deliveries/{id}detail with the log of each attempt
POST /v1/webhooks/deliveries/{id}/retryredelivers now, with the same event_id (up to 5 manual redeliveries)

Was your endpoint down? List the exhausted deliveries since the incident and redeliver them — deduplication by event_id guarantees nothing is processed twice.

6. Recipe: syncing an external system with Stevo​

  1. Create an API Key with instances:read, messages:send, messages:read, webhooks:read and webhooks:write.
  2. Create the instances with external_ref = the customer id in your system.
  3. Create one account webhook and store the secret.
  4. In the endpoint: verify the signature, deduplicate by event_id, answer 200 quickly and process in a queue — use instance.external_ref to know which customer the event belongs to.
  5. Periodically (e.g. every hour), run reconciliation with updated_since to catch any missed change, including deletions.
  6. Send with Idempotency-Key; on 504 upstream_timeout, check the status by key before resending.
Never access Stevo's database

External integrations use only the API (or the SDK). Supabase/Postgres, service_role and internal tables are not part of the contract and may change without notice.

7. MCP — the same API for AI agents​

The same backend exposes an MCP server at https://openapi.stevo.chat/mcp (Streamable HTTP), with the same operations as tools and authenticated by the same API key. Plug it into n8n (MCP Client node), Claude or Cursor.

References​