Skip to content
Snap AI Snap · Understand · Simplify
Features Use Cases How It Works FAQ
Get the App

Developer API

SnapAI · Last updated 23 September 2026

  • Privacy Policy
  • Terms of Service
  • Support
  • Delete your account

Connect your business WhatsApp number in the SnapAI app, generate an API key, and your own servers can send one-time codes and transactional messages from that number. Messages come from your WhatsApp account, under your business's name — this API is the front door to it, not a separate sender.

This is your WhatsApp number, and WhatsApp's rules apply to it. Only message people who have agreed to hear from you, keep to one-to-one transactional messages and one-time codes, and never use this API for marketing or bulk sending. A business that ignores that gets its number banned by WhatsApp — that ban comes from WhatsApp, not from us, and we cannot lift it. Use the Broadcast tool in the app for anything that goes to many people; it is built for it and it warms your number up properly.

On this page

  1. Getting started
  2. Authentication
  3. Sending an OTP
  4. Verifying an OTP
  5. Transactional messages
  6. Delivery status
  7. Your numbers
  8. Webhooks
  9. Idempotency
  10. Rate limits & quota
  11. Errors
  12. Acceptable use

Getting started

  1. Open SnapAI and connect your business WhatsApp number (WhatsApp → Numbers).
  2. Go to WhatsApp → Developer API and tap New key.
  3. Give the key a name, pick its scopes, and copy the key. It is shown once.
  4. Store it as a server-side secret. Never ship it in a mobile app or a browser.

Base URL:

https://snapai.reviewindia.org/api/v1

Every response uses the same envelope, on success and on failure alike:

{ "ok": true,  "data": { … } }
{ "ok": false, "error": { "code": "…", "message": "…", "retryable": false } }

Authentication

Send your key as a bearer token. A key looks like snap_live_<prefix>_<secret>; the prefix is safe to log, the secret is not.

Authorization: Bearer snap_live_a1b2c3d4e5f6_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Each key carries scopes, and may only do what they name:

ScopeAllows
whatsapp.otp.sendSending and verifying one-time codes
whatsapp.messages.sendSending transactional messages
whatsapp.status.readReading delivery status and your connected numbers
whatsapp.webhooks.writeRegistering callback endpoints

Keys can be rotated from the app. The old secret keeps working for one hour, so you can roll a deployment without downtime. They can be revoked instantly, and a revoked key can never be restored.

Your keys work while your SnapAI Business plan is active. If the plan lapses, every call answers 403 feature_disabled with a message saying so; renewing the plan makes the same keys work again.

Sending an OTP

POST/v1/whatsapp/otp/send

FieldType
tostring, requiredDestination in international format, e.g. +919888877666
codestring4–8 digits. Omit it and we generate a 6-digit code.
templatestringMessage text. Must contain {code}. {brand} and {minutes} are also substituted.
brandNamestringYour business name in the message. Up to 40 characters.
expiresInSecondsinteger60–900. Default 300.
connectionIdstringWhich of your numbers to send from. Defaults to the first connected one.
curl
curl -X POST https://snapai.reviewindia.org/api/v1/whatsapp/otp/send \
  -H "Authorization: Bearer $SNAPAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: signin-8f21c0" \
  -d '{
    "to": "+919888877666",
    "brandName": "Acme",
    "expiresInSeconds": 300
  }'
JavaScript
const res = await fetch("https://snapai.reviewindia.org/api/v1/whatsapp/otp/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SNAPAI_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `signin-${sessionId}`,
  },
  body: JSON.stringify({ to: "+919888877666", brandName: "Acme" }),
});

const body = await res.json();
if (!body.ok) throw new Error(body.error.message);

const { requestId, expiresAt } = body.data;
PHP
$ch = curl_init('https://snapai.reviewindia.org/api/v1/whatsapp/otp/send');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer '.getenv('SNAPAI_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: signin-'.$sessionId,
    ],
    CURLOPT_POSTFIELDS => json_encode(['to' => '+919888877666', 'brandName' => 'Acme']),
]);

$body = json_decode(curl_exec($ch), true);
if (! $body['ok']) {
    throw new RuntimeException($body['error']['message']);
}

$requestId = $body['data']['requestId'];

202 Accepted

{
  "ok": true,
  "data": {
    "requestId": "01a0cb83-c448-70f6-853b-4004b7aced92",
    "to": "+91988••••666",
    "status": "sent",
    "messageId": "01a0cb83-c4ba-729a-8f54-b344b4e54ba0",
    "expiresAt": "2026-09-23T10:05:00+00:00",
    "attemptsAllowed": 5
  }
}

Keep the requestId on the session you are signing in. The code itself never comes back — we store only a hash of it, so neither we nor anyone reading our database can complete your user's sign-in.

Verifying an OTP

POST/v1/whatsapp/otp/verify

Send requestId (preferred) or to, plus the code your user typed.

curl -X POST https://snapai.reviewindia.org/api/v1/whatsapp/otp/verify \
  -H "Authorization: Bearer $SNAPAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "requestId": "01a0cb83-…", "code": "482915" }'
{ "ok": true, "data": { "verified": true, "reason": null, "attemptsRemaining": 0,
                        "requestId": "01a0cb83-…" } }

A wrong code is 200 with verified: false, not an error status — a mistyped code is a normal thing for a sign-in screen to handle. Read reason:

reason
invalid_codeWrong code. attemptsRemaining says how many tries are left.
expiredThe code is past its expiry. Send a new one.
already_usedCodes are single use.
too_many_attemptsFive wrong guesses destroy the code. Send a new one.
not_foundNo such request, or it belongs to a different key's account.

Asking for a new code for the same number invalidates the previous one, so there is never more than one live code per phone.

Transactional messages

POST/v1/whatsapp/messages

One recipient per call. There is no list parameter. For anything that goes to many people, use Broadcasts in the app.

FieldType
tostring, requiredInternational format.
textstringUp to 4096 characters. Required unless media is present.
media.urlstringA public https:// URL we fetch. Max 16 MB.
media.base64stringThe file inline. A data: prefix is stripped for you.
media.captionstringUsed as the message text when text is absent.
media.fileNamestringThe name the recipient sees.
quotedMessageIdstringReply to an earlier message.

The file type is detected from the bytes, not from what you declare. Photos, video, audio, PDF, DOCX, TXT and CSV are carried; anything else is refused with unsupported_media_type.

curl -X POST https://snapai.reviewindia.org/api/v1/whatsapp/messages \
  -H "Authorization: Bearer $SNAPAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-shipped" \
  -d '{
    "to": "+919888877666",
    "text": "Your order #1042 has shipped. Track it at acme.example/t/1042"
  }'
{
  "ok": true,
  "data": {
    "messageId": "01a0cb83-…",
    "to": "+91988••••666",
    "kind": "text",
    "mediaType": null,
    "status": "sent",
    "sentAt": "2026-09-23T10:00:00+00:00",
    "deliveredAt": null,
    "readAt": null
  }
}

Delivery status

GET/v1/whatsapp/messages/{id}

curl https://snapai.reviewindia.org/api/v1/whatsapp/messages/01a0cb83-… \
  -H "Authorization: Bearer $SNAPAI_API_KEY"

status is one of pending, sent, delivered, read, failed, and only ever moves forward. Register a webhook rather than polling — it is faster, and it does not spend your rate limit.

Your numbers

GET/v1/whatsapp/numbers

{
  "ok": true,
  "data": {
    "numbers": [
      { "id": "01a0cb83-…", "number": "+919876543210", "displayName": "Acme Support",
        "status": "connected", "canSend": true, "lastCheckedAt": "2026-09-23T09:58:00+00:00" }
    ],
    "allowance": { "limit": 2000, "used": 128, "remaining": 1872,
                   "resetsAt": "2026-10-01T00:00:00+00:00", "period": "2026-09" }
  }
}

One call that answers "is my integration going to work right now" without sending anybody a message to find out.

Webhooks

POST/v1/webhooks

curl -X POST https://snapai.reviewindia.org/api/v1/webhooks \
  -H "Authorization: Bearer $SNAPAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.acme.example/snapai",
        "events": ["whatsapp.message.status"] }'

The response carries a signing secret (whsec_…) once. Store it; it cannot be read back. Use ["*"] for every event.

Each delivery is a POST to your URL:

POST /snapai HTTP/1.1
X-SnapAI-Timestamp: 1758617400
X-SnapAI-Signature: sha256=6b1f…
X-SnapAI-Event: whatsapp.message.status
X-SnapAI-Delivery: 01a0cb83-…

{ "event": "whatsapp.message.status",
  "deliveryId": "01a0cb83-…",
  "occurredAt": "2026-09-23T10:01:02+00:00",
  "data": { "messageId": "01a0cb83-…", "to": "+91988••••666",
            "status": "delivered", "deliveredAt": "2026-09-23T10:01:02+00:00",
            "readAt": null } }

Verifying the signature

Compute HMAC-SHA256 over <X-SnapAI-Timestamp> + "." + <raw request body> with your webhook secret, and compare it to the hex after sha256= using a constant-time comparison. Reject any request whose timestamp is more than five minutes old — that is what stops a captured delivery being replayed at you.

Node
import crypto from "node:crypto";

export function verify(rawBody, headers, secret) {
  const ts  = headers["x-snapai-timestamp"];
  const sig = headers["x-snapai-signature"] ?? "";

  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
PHP
function snapaiWebhookIsValid(string $rawBody, array $headers, string $secret): bool
{
    $timestamp = $headers['X-SnapAI-Timestamp'] ?? '';
    $signature = $headers['X-SnapAI-Signature'] ?? '';

    if (abs(time() - (int) $timestamp) > 300) {
        return false;
    }

    $expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);

    return hash_equals($expected, $signature);
}

Answer 2xx quickly. A non-2xx (or a timeout past 8 seconds) is retried after 30 s, 2 m, 10 m and 1 h. After five consecutive failures the delivery is dead-lettered and the endpoint is switched off — you will see it as inactive in the app, and you re-enable it by registering it again. Deliveries carry ids, statuses and masked numbers only: message text and one-time codes are never sent to a webhook.

GET/v1/webhooks lists your endpoints. DELETE/v1/webhooks/{id} removes one.

Idempotency

Send an Idempotency-Key header on any POST. If you retry with the same key we return the first result instead of sending a second message, and set Idempotent-Replayed: true. Use something derived from your own record — an order id, a sign-in session id.

  • Keys are remembered for 24 hours, per API key.
  • The same key with a different body is 409. Use a new key when the request changes.
  • A failed call is not remembered, so you can fix the request and retry with the same key.

Rate limits & quota

LimitDefault
All calls, per key60 per minute
Sends (OTP or message), per key30 per minute
OTP codes to one number5 per minute, 15 per hour
OTP verifications for one number15 per 10 minutes

Over a limit is 429 rate_limited. On top of that, every message spends one of your plan's monthly WhatsApp messages — the same allowance the app's own replies spend, because it is the same WhatsApp number. Running out is 429 quota_exceeded, with the figures on the error object:

{ "ok": false,
  "error": { "code": "quota_exceeded", "retryable": false,
             "message": "Your plan includes 2000 WhatsApp messages a month and all of them are used. …",
             "limit": 2000, "used": 2000, "remaining": 0,
             "resetsAt": "2026-10-01T00:00:00+00:00" } }

Errors

HTTPerror.codeWhat to do
401unauthenticatedThe key is wrong, revoked or expired. Generate a new one in the app.
403feature_disabledMissing scope, a lapsed plan, or a number that has opted out. The message says which.
404not_foundNo such message, webhook or chat.
409not_configuredNo connected WhatsApp number, or it needs reconnecting in the app.
409unknownAn Idempotency-Key conflict — see above.
422unknownA field is missing or malformed. The message names it.
422invalid_image / image_too_large / unsupported_media_typeThe file cannot be carried. Max 16 MB.
429rate_limitedBack off and retry.
429quota_exceededThe month's allowance is used.
503feature_disabled / networkThe API or WhatsApp is briefly unavailable. Retry shortly.

error.retryable tells you whether repeating the identical request could ever succeed.

Acceptable use

  • Consent first. Only message people who asked to hear from you.
  • Transactional only. One-time codes, confirmations, reminders, receipts, support replies.
  • No marketing through the OTP endpoint. It refuses links, offers and promotional wording — and that refusal is protecting your number, not ours.
  • Honour opt-outs. Anyone you have blocked in the app, or who replied STOP, is refused by the API.
  • Keep your key secret. Server-side only. If it leaks, rotate or revoke it in the app immediately.

You are the sender. WhatsApp's Business Policy and Commerce Policy apply to your number and your messages, and a business that breaks them loses its number. We cannot appeal that on your behalf.

More

  • Help & support
  • Terms of Service
  • Privacy Policy
Snap AI Snap · Understand · Simplify

Screenshots, photos, PDFs and bills, explained in plain language.

Product

  • Features
  • How it works
  • Download
  • Developer API

Support

  • Help & support
  • FAQ
  • Contact
  • Delete account

Legal

  • Privacy Policy
  • Terms of Service
© 2026 Snap AI. All rights reserved. Snap a better, simpler tomorrow.