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.inQuickstart
Reminders are the fastest way to start. The next section walks the full commitment lifecycle; this one sends a one-off template message.
- Create a key in Settings → API keys. It is shown once.
- Send a pre-approved template to a phone number.
- Poll the message, or point a webhook at your server for status updates.
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"
}'{ "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.
/api/v1/reminders| Field | Type | Notes |
|---|---|---|
schedule_id | uuid | Required. The reminder schedule that defines when messages go out. |
external_id | string | Required. Your stable ID for this commitment. Replay with the same value upserts. |
event_type | string | Required. e.g. appointment. |
to | string | Required. Recipient phone, digits plus optional +. |
title | string | Required. Shown in message parameters. |
scheduled_at | string | Required. Future ISO instant with Z or numeric offset. |
timezone | string | Required IANA zone (e.g. Asia/Kolkata). Controls local wall-clock rules; never infer it from a numeric offset. |
metadata | object | Optional. Free-form fields for message parameters. |
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.
{
"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"):
{
"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_xxxA 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:
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
/api/v1/messagesTemplates are the only way to start a conversation. They must already be approved in your WhatsApp Business account.
| Field | Type | Notes |
|---|---|---|
to | string | Required. Digits, spaces and + are all accepted. |
template | string | Required unless you send text. |
language | string | Optional, defaults to en. Must match the approved template. |
params | string[] | Optional. Positional body variables, in {{1}} order. |
header_image_url | string | Optional. 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
/api/v1/messagesInside 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.
{
"to": "919876543210",
"text": "Your order ships tomorrow."
}{
"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
/api/v1/messages/{id}Use the id returned by the send — a numeric queue id for templates, a wamid. for session replies.
{
"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.
{
"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.
{
"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.
| Header | Value |
|---|---|
X-Webhook-Timestamp | ISO-8601 UTC, identical to data.timestamp |
X-Webhook-Signature | sha256= + 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.
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)
})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 "", 200Errors
Errors are JSON with an error message, and a stable code where you may want to branch on it.
| Status | Code | Meaning |
|---|---|---|
| 200 | — | Session reply sent. |
| 202 | — | Template queued. |
| 400 | — | Validation failed, including both or neither of template / text. |
| 401 | — | Missing, unknown, or revoked key. |
| 402 / 403 | — | Plan inactive, or the feature is off for your workspace. |
| 404 | — | No such message in your workspace. |
| 409 | opted_out | The contact has opted out of messages. |
| 409 | outside_service_window | The 24-hour window closed. Send a template. |
| 429 | daily_limit_reached | Daily API send limit hit. Includes limit and used. |
| 503 | — | Temporarily 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.
{
"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:
- Set your webhook URL to an n8n Webhook node and generate a secret.
- Ask us to switch your workspace to external inbound routing.
- Verify the signature, filter on
type === "message.inbound". - Reply with an HTTP Request node posting
{ to, text }back to/api/v1/messages.
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.