Developers

WhatsApp API

Replay your source system's commitments — bookings, appointments, follow-ups — as scheduled WhatsApp reminders with idempotent create, reschedule and cancel. Also send one-off template and session messages and receive signed delivery webhooks. Everything is JSON over HTTPS — no SDK required.

Base URL

https://whatsapp.sambhavtech.in
Machine-readable contractopenapi.json

Quickstart

Reminders are the fastest way to start. The next section walks the full commitment lifecycle; this one sends a one-off template message.

  1. Create a key in Settings → API keys. It is shown once.
  2. Send a pre-approved template to a phone number.
  3. Poll the message, or point a webhook at your server for status updates.
cURL
curl -X POST https://whatsapp.sambhavtech.in/api/v1/messages \
  -H "Authorization: Bearer wsk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "919876543210",
    "template": "order_confirmation",
    "language": "en_US",
    "params": ["Gagan", "#1042"],
    "header_image_url": "https://cdn.example.com/orders/1042.jpg"
  }'
Response · 202
{ "id": "48213", "status": "queued" }

Commitment reminders

A commitment is an event in your system — an appointment, a class, a payment follow-up — with a stable external_id and a schedule of reminders leading up to it. Replaying the same event never duplicates a reminder; changing its material supersedes the old generation. When a booking is cancelled, one call stops every pending reminder.

POST/api/v1/reminders
FieldTypeNotes
schedule_iduuidRequired. The reminder schedule that defines when messages go out.
external_idstringRequired. Your stable ID for this commitment. Replay with the same value upserts.
event_typestringRequired. e.g. appointment.
tostringRequired. Recipient phone, digits plus optional +.
titlestringRequired. Shown in message parameters.
scheduled_atstringRequired. Future ISO instant with Z or numeric offset.
timezonestringRequired IANA zone (e.g. Asia/Kolkata). Controls local wall-clock rules; never infer it from a numeric offset.
metadataobjectOptional. Free-form fields for message parameters.
cURL
curl -X POST https://whatsapp.sambhavtech.in/api/v1/reminders \
  -H "Authorization: Bearer wsk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "schedule_id": "df75125c-2138-47c8-bc85-34080305dc59",
    "external_id": "booking-1042",
    "event_type": "appointment",
    "to": "+919876543210",
    "title": "Product consultation",
    "scheduled_at": "2030-09-10T15:30:00+05:30",
    "timezone": "Asia/Kolkata",
    "metadata": { "company_name": "Example Ltd" }
  }'

A new commitment returns 201 with the generated jobs. Replaying the same external_id and material returns 200 with created: false and no new jobs; sending changed material creates one superseding generation.

Response · 201
{
  "id": "7282ea21-f012-4db3-a9f3-11cb14cb2491",
  "external_id": "booking-1042",
  "status": "scheduled",
  "generation": 1,
  "created": true,
  "jobs": [
    {
      "id": "0df7b128-c3cf-4cea-a74c-343486f0315a",
      "label": "9 AM one day before",
      "scheduled_for": "2030-09-09T09:00:00.000+05:30",
      "status": "queued",
      "skip_reason": null
    }
  ]
}

Local wall-clock schedules

A schedule's offsets may be exact durations (legacy, e.g. 24 hours before) or calendar rules with a local time. Calendar rules carry at_local_time (strict HH:mm, only with unit: "days"):

JSON
{
  "key": "day-before-morning",
  "value": 1,
  "unit": "days",
  "at_local_time": "09:00",
  "label": "9 AM one day before"
}

at_local_time is resolved in the event's IANA timezone. Across a daylight-saving transition the reminder stays at the requested wall clock: during a spring-forward gap the resolved time moves forward, and during a fall-back overlap the earlier occurrence is used. Without at_local_time, 1 day remains exactly 24 hours.

Lifecycle

Read an event and its jobs with GET /api/v1/reminders/{id}. Reschedule with POST /api/v1/reminders/{id}/reschedule. Cancel with POST /api/v1/reminders/{id}/cancel. Record what actually happened with POST /api/v1/reminders/{id}/outcome (attended, no_show, or cancelled). See the full semantics in the Reminder API guide and the OpenAPI contract above.

Authentication

Every request carries a bearer key. Keys start with wsk_live_, belong to one workspace, and are stored only as a hash — if you lose one, revoke it and create another.

Authorization: Bearer wsk_live_xxx

A missing, unknown, or revoked key returns 401. Never ship a key in browser or mobile code; call this API from your server.

OpenAPI contract

A machine-readable OpenAPI 3.1 document describes every shipped public route, schema, and status code. Generate a typed client or import it into Postman:

/openapi.json

Documented routes: POST /api/v1/messages, GET /api/v1/messages/{id}, POST /api/v1/reminders, GET /api/v1/reminders/{id}, POST /api/v1/reminders/{id}/cancel, POST /api/v1/reminders/{id}/reschedule, and POST /api/v1/reminders/{id}/outcome.

Send a template message

POST/api/v1/messages

Templates are the only way to start a conversation. They must already be approved in your WhatsApp Business account.

FieldTypeNotes
tostringRequired. Digits, spaces and + are all accepted.
templatestringRequired unless you send text.
languagestringOptional, defaults to en. Must match the approved template.
paramsstring[]Optional. Positional body variables, in {{1}} order.
header_image_urlstringOptional. Public HTTPS JPEG/PNG for a template approved with an image header.

A 202 means queued, not delivered. Template sends run ahead of bulk broadcasts, so a campaign in progress will not delay a transactional message.

Send a session reply

POST/api/v1/messages

Inside 24 hours of a customer's last message you may reply with free-form text — no template, no approval. Send text instead of template; supplying both is a 400.

JSON
{
  "to": "919876543210",
  "text": "Your order ships tomorrow."
}
Response · 200
{
  "id": "wamid.HBgMOTE...",
  "status": "sent",
  "message_id": "wamid.HBgMOTE...",
  "to": "919876543210"
}

This sends immediately rather than queueing, because a queued session message could only expire. If the 24-hour window has closed you get 409 outside_service_window — send a template to reopen it.

Session replies have no queue id, so id is the WhatsApp message id instead. Pass it to GET /api/v1/messages/{id} or match it against message.status events exactly as you would a template id.

Check a message

GET/api/v1/messages/{id}

Use the id returned by the send — a numeric queue id for templates, a wamid. for session replies.

Response · 200
{
  "id": "48213",
  "status": "delivered",
  "to": "919876543210",
  "template": "order_confirmation",
  "language": "en_US",
  "attempts": 1,
  "error": null,
  "created_at": "2026-08-04T09:25:55.180Z",
  "sent_at": "2026-08-04T09:26:03.471Z",
  "delivered_at": "2026-08-04T09:26:03.000Z",
  "read_at": null
}

status is one of queued, sending, sent, delivered, read, failed, canceled. Keys only ever see their own workspace's messages; anything else returns 404.

Webhooks

Set one HTTPS URL in Settings → Delivery webhook and generate a signing secret. Both event types arrive at that one URL — switch on type.

message.status

Fires as a message you sent progresses. template is null for session replies.

JSON
{
  "type": "message.status",
  "data": {
    "id": "48213",
    "status": "delivered",
    "to": "919876543210",
    "template": "order_confirmation",
    "timestamp": "2026-08-04T09:26:03.000Z",
    "error": null
  }
}

message.inbound

Fires when a customer messages you — this is what an external bot listens to. It is only sent when your workspace is set to route inbound replies externally; otherwise the built-in bot builder answers and nothing is relayed. Exactly one of the two responds, so a customer never receives duplicate replies.

JSON
{
  "type": "message.inbound",
  "data": {
    "from": "919876543210",
    "name": "Gagan Thakur",
    "message_id": "wamid.HBgMOTE...",
    "type": "text",
    "text": "where is my order",
    "timestamp": "2026-08-04T10:33:03.000Z"
  }
}

Every delivery is persisted and retried with backoff until your endpoint returns a fast 2xx, up to 5 attempts. A slow or failing endpoint never blocks message processing, but should still reply 200 quickly and do work asynchronously. A delivery that exhausts its attempts is marked failed and can be retried manually; you can always reconcile state with GET /api/v1/messages/{id}.

Verifying signatures

When a secret is set, every delivery carries two headers.

HeaderValue
X-Webhook-TimestampISO-8601 UTC, identical to data.timestamp
X-Webhook-Signaturesha256= + HMAC-SHA256 of `${timestamp}.${rawBody}`

Sign the raw body, before any JSON parsing — re-serialising changes the bytes and the signature will never match. Compare in constant time.

Node.js / Express
const crypto = require('crypto')

app.post('/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const timestamp = req.get('X-Webhook-Timestamp')
    const signature = req.get('X-Webhook-Signature')

    const expected = 'sha256=' + crypto
      .createHmac('sha256', process.env.WEBHOOK_SECRET)
      .update(timestamp + '.' + req.body.toString())
      .digest('hex')

    const ok = signature && signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))

    if (!ok) return res.sendStatus(401)

    const event = JSON.parse(req.body.toString())
    res.sendStatus(200)          // acknowledge first

    if (event.type === 'message.inbound') handleReply(event.data)
  })
Python / Flask
import hmac, hashlib, os
from flask import request, abort

@app.post("/webhook")
def webhook():
    raw = request.get_data()
    timestamp = request.headers.get("X-Webhook-Timestamp", "")
    signature = request.headers.get("X-Webhook-Signature", "")

    expected = "sha256=" + hmac.new(
        os.environ["WEBHOOK_SECRET"].encode(),
        f"{timestamp}.".encode() + raw,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        abort(401)

    event = request.get_json()
    return "", 200

Errors

Errors are JSON with an error message, and a stable code where you may want to branch on it.

StatusCodeMeaning
200Session reply sent.
202Template queued.
400Validation failed, including both or neither of template / text.
401Missing, unknown, or revoked key.
402 / 403Plan inactive, or the feature is off for your workspace.
404No such message in your workspace.
409opted_outThe contact has opted out of messages.
409outside_service_windowThe 24-hour window closed. Send a template.
429daily_limit_reachedDaily API send limit hit. Includes limit and used.
503Temporarily unavailable. Retry with backoff.

Limits

API sends have their own daily budget, counted separately from broadcasts — a large campaign can never eat the quota your backend depends on, and vice versa. The counter resets at 00:00 UTC.

Response · 429
{
  "error": "Daily API send limit of 1000 reached",
  "code": "daily_limit_reached",
  "limit": 1000,
  "used": 1000
}

Your current usage is on the dashboard. Need more? See pricing.

Building a bot with n8n

Use the built-in bot builder for menu-style flows. For anything that needs your own logic — a database lookup, an LLM, an order system — drive it externally:

  1. Set your webhook URL to an n8n Webhook node and generate a secret.
  2. Ask us to switch your workspace to external inbound routing.
  3. Verify the signature, filter on type === "message.inbound".
  4. Reply with an HTTP Request node posting { to, text } back to /api/v1/messages.
n8n · HTTP Request node
POST https://whatsapp.sambhavtech.in/api/v1/messages

Headers
  Authorization: Bearer wsk_live_xxx
  Content-Type: application/json

Body
  {
    "to": "{{ $json.data.from }}",
    "text": "{{ $json.reply }}"
  }

Because the reply lands inside the 24-hour window, no template approval is needed — you can change what your bot says as often as you like.