API Documentation

Send and receive WhatsApp messages from your application via a single REST API. SAGE-API handles instance lifecycle, message routing, and webhook delivery — you authenticate with one key and call simple HTTP endpoints.

v1Base URL: https://sageai.kavachbrowser.com

Getting started

Three steps to send your first message:

  1. Create an account and verify your email.
  2. Provision an instance, scan the QR code with WhatsApp on your phone to pair it.
  3. Mint an API key. Plaintext is shown once — store it in your secrets manager.

Then use that key in the Authorization header on every API call.

Authentication

All API calls under /api/v1/instance/{name}/* require a SAGE-API key in theAuthorization header as a Bearer token.

Authorization: Bearer sage_kf3hJ2...XYz

Keys are scoped to your account but can call any of your instances. They are not scoped per-instance in v1. Revoke a key from the API keys page at any time.

Where the key goes: server-side only. Never expose sage_ keys in client-side JavaScript, mobile apps, or browser extensions. Keys grant full send + manage rights to your account.

Send a text message

POST/api/v1/instance/{name}/message/sendText
Send a plain-text WhatsApp message
Replace {name} with your instance name (e.g. acme-prod).

Request body

{
  "number": "919876543210",
  "text": "Hello from SAGE-API!",
  "delay": 0
}

number — country code + number, digits only (10–15 digits).delay — optional ms to wait before sending (max 60000).

Example

curl -X POST 'https://sageai.kavachbrowser.com/api/v1/instance/acme-prod/message/sendText' \
  -H 'Authorization: Bearer sage_yourkey' \
  -H 'Content-Type: application/json' \
  -d '{
    "number": "919876543210",
    "text": "Hello from SAGE-API!"
  }'

Response (201)

{
  "key": { "remoteJid": "919876543210@s.whatsapp.net", "fromMe": true, "id": "..." },
  "message": { "extendedTextMessage": { "text": "Hello from SAGE-API!" } },
  "messageTimestamp": 1735689600,
  "status": "PENDING"
}

Send media

POST/api/v1/instance/{name}/message/sendMedia
Send an image, video, audio, or document by URL
The media URL must be publicly fetchable from the SAGE-API server.

Request body

{
  "number": "919876543210",
  "mediatype": "image",
  "media": "https://example.com/photo.jpg",
  "caption": "Check this out!",
  "fileName": "photo.jpg"
}

mediatype — one of image, video, audio, document.fileName — required for documents.

Example

curl -X POST 'https://sageai.kavachbrowser.com/api/v1/instance/acme-prod/message/sendMedia' \
  -H 'Authorization: Bearer sage_yourkey' \
  -H 'Content-Type: application/json' \
  -d '{
    "number": "919876543210",
    "mediatype": "image",
    "media": "https://example.com/photo.jpg",
    "caption": "Today's special"
  }'

Send a location

POST/api/v1/instance/{name}/message/sendLocation
Send a pinpointed location

Request body

{
  "number": "919876543210",
  "latitude": 28.6139,
  "longitude": 77.2090,
  "name": "Connaught Place",
  "address": "New Delhi, India"
}

Send a contact card

POST/api/v1/instance/{name}/message/sendContact
Send a vCard with one or more contacts

Request body

{
  "number": "919876543210",
  "contact": [
    {
      "fullName": "Alice Example",
      "phoneNumber": "+91 98765 43210",
      "organization": "Acme Cafe"
    }
  ]
}

Rate limits

Every authed call against /api/v1/instance/{name}/* goes through a per-key sliding-window rate limit. The default cap is 60 requests / minute / API key. You can lower it when minting a key (Advanced → Rate limit) or edit it later.

Every response carries the current limit state in headers, so you don't have to guess:

X-RateLimit-Limit:     60
X-RateLimit-Remaining: 47
Retry-After:           24    # only on 429 responses

When the bucket empties you get a 429:

HTTP/1.1 429 Too Many Requests
Retry-After: 24
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0

{ "error": "rate_limited" }

Recommended back-off

  • Honour Retry-After — sleep that many seconds before retrying.
  • If you're running at scale, watch X-RateLimit-Remaining and slow yourself down before the limiter has to refuse you.
  • Per-IP and per-email limits also exist on /api/v1/auth/* (10 attempts / 10 min from an IP, 5 attempts / 10 min for the same email). Same 429 shape.

Need a higher cap or per-instance scope? Edit the key on the API keys page — you can pin a key to specific instances, raise / lower its rate limit, and / or restrict it to a CIDR allowlist of source IPs (only requests from those IPs validate).

Webhooks (receive events)

Configure a webhook URL on the Webhooks page and SAGE-API will POST events to it as JSON. Events include incoming messages, message-status updates, connection state, and group changes.

Example payload (incoming message)

{
  "event": "MESSAGES_UPSERT",
  "instance": "acme-prod",
  "data": {
    "key": { "remoteJid": "919999999999@s.whatsapp.net", "fromMe": false, "id": "..." },
    "pushName": "Alice",
    "messageType": "conversation",
    "message": { "conversation": "Hello!" },
    "messageTimestamp": 1735689600
  }
}

You can subscribe to a subset of event types. The most common ones are MESSAGES_UPSERT, CONNECTION_UPDATE, and SEND_MESSAGE_UPDATE.

Verifying webhook signatures

Every outbound delivery from SAGE-API carries an HMAC signature so you can confirm the request came from us and wasn't replayed. Headers on every POST:

X-Sage-Signature: sha256=<hex>
X-Sage-Timestamp: 1715234567
X-Sage-Instance:  acme-prod

The signature is HMAC-SHA256(secret, "{timestamp}.{raw_body}"). Including the timestamp prevents replay; we recommend a 5-minute drift window.

v1: platform-wide signing key

Today every customer's deliveries are signed with one platform-wide HMAC key — contact support to receive yours. The per-instance Reveal / Rotate UI is staged for a follow-up release that switches signing to a per-instance key (the dashboard secret will become live then). Until then, the per-instance secret is reserved for that future cutover — verify with the platform key.

Node 18+

import crypto from 'node:crypto';

export function verifySageWebhook(req, secret) {
  const ts = req.headers['x-sage-timestamp'];
  const sig = req.headers['x-sage-signature'];
  if (!ts || !sig?.startsWith('sha256=')) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;  // 5-min replay window
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${req.rawBody}`)  // raw body — NOT a JSON.parse round-trip
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(sig.slice(7), 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

Python 3.10+

import hmac, hashlib, time

def verify_sage_webhook(headers, raw_body, secret) -> bool:
    ts = headers.get("x-sage-timestamp")
    sig = headers.get("x-sage-signature", "")
    if not ts or not sig.startswith("sha256="):
        return False
    if abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(
        secret.encode(),
        f"{ts}.{raw_body.decode() if isinstance(raw_body, bytes) else raw_body}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(mac, sig[7:])

Common pitfalls

  • Use the raw request body in the HMAC, not a JSON.parse JSON.stringify round-trip. Re-serialization changes whitespace and key order; the MAC will not match.
  • Compare with a constant-time compare (timingSafeEqual / hmac.compare_digest), not ===.
  • Reject any request older than 5 minutes (x-sage-timestamp drift) to prevent replay.
  • After rotating the secret in the dashboard, the old value stops verifying immediately. Update your verifier within 30s.

Instance management (account-plane)

These endpoints use your session cookie from the dashboard, not a SAGE-API key. They're meant for dashboard automation; from your servers, prefer the data-plane endpoints above. List + create instances live under /api/v1/me/instances.

GET/api/v1/me/instances
List your instances
{
  "instances": [
    {
      "evolution_instance_name": "acme-prod",
      "display_name": "Acme Production",
      "status": "paired",
      "created_at": "2026-05-08T10:00:00Z",
      "paired_at": "2026-05-08T10:01:23Z"
    }
  ]
}

System status

Public health endpoint for uptime monitors. Always responds 200 — trigger your alert on the body's status field, not the HTTP code (a non-200 here would page the marketing site too).

GET/api/v1/health
Health snapshot
Public, always 200. Auth not required.
{
  "status": "ok",
  "version": "abc1234",
  "db_ms": 12,
  "sage_api": true,
  "sage_api_ms": 412,
  "ts": "2026-05-09T12:34:56.789Z"
}

status is "ok" when the BFF can reach Supabase and the WhatsApp gateway, otherwise "degraded". version is the deployed git SHA. Edge-cached for 10 seconds.

Errors

All errors come back as JSON with an error field and an HTTP status code.

StatusCodeMeaning
400invalid_bodyRequest body failed validation. detail contains zod errors.
400invalid_urlWebhook URL must resolve to a public IP. We block private CIDRs (RFC1918, link-local, loopback) to prevent SSRF.
401unauthorizedMissing, revoked, or IP-allowlist-rejected API key. Caller IP must match the key's allowlist when one is configured.
402plan_requiredEndpoint requires a higher plan tier.
403email_unverifiedVerify your email before this action.
403forbiddenKey valid, but it can't access this instance. Either the instance isn't yours, or the key is scoped to a different set of instances.
404not_foundInstance, broadcast, or template not found.
409email_already_registeredSign-up attempted with an existing email.
429rate_limitedPer-key, per-IP, or per-email cap exceeded. Honour Retry-After — see Rate limits.
502sage_api_failedUpstream WhatsApp gateway error. detail usually has the cause.

Ready to send your first message?

Need help? Contact support · Credits