# AI agents and coding assistants Source: https://tawked.mintlify.app/agents Give Claude Code, Codex, Cursor or any agent the Tawked reference: MCP server, Markdown pages, llms.txt, skill.md and the OpenAPI document. Everything on this site is published for machines as well as people. Point your coding agent at it once and it can look up the exact request shapes, error codes and limits while it writes your integration. ## Connect the docs MCP server The site hosts a Model Context Protocol server at `https://docs.tawked.com/mcp`. It gives an agent three tools: search the docs, read any page or section, and report a problem back to us. No key, no account. ```bash theme={null} claude mcp add --transport http tawked https://docs.tawked.com/mcp ``` Add to `~/.codex/config.toml`: ```toml theme={null} [mcp_servers.tawked] type = "http" url = "https://docs.tawked.com/mcp" ``` Add to `.cursor/mcp.json` in your project, or the global `mcp.json`: ```json theme={null} { "mcpServers": { "tawked": { "url": "https://docs.tawked.com/mcp" } } } ``` Add to `.vscode/mcp.json`: ```json theme={null} { "servers": { "tawked": { "type": "http", "url": "https://docs.tawked.com/mcp" } } } ``` Add to `mcp_config.json`: ```json theme={null} { "mcpServers": { "tawked": { "serverUrl": "https://docs.tawked.com/mcp" } } } ``` Add to `opencode.json`: ```json theme={null} { "mcp": { "tawked": { "type": "remote", "url": "https://docs.tawked.com/mcp", "enabled": true } } } ``` Settings, then **Connectors**, **Add custom connector**, URL `https://docs.tawked.com/mcp`. The same options sit behind the menu at the top of every page: copy the page as Markdown, open it in ChatGPT or Claude, add the MCP server to Cursor or VS Code in one click, or download the OpenAPI document from any reference page. ## Install the skill Agents that support skills can load a generated `skill.md` that describes what Tawked does and how the calls fit together: ```bash theme={null} npx skills add https://docs.tawked.com ``` It is regenerated from these pages, so it never goes stale. ## Read pages as Markdown Every page is available as clean Markdown. Append `.md` to its URL, or ask for it with an `Accept` header: ```bash theme={null} curl -L -H "Accept: text/markdown" https://docs.tawked.com/quickstart ``` Two index files cover the whole site: [`llms.txt`](https://docs.tawked.com/llms.txt) lists every page with a description, and [`llms-full.txt`](https://docs.tawked.com/llms-full.txt) is the whole site in one file for a context window. ## The OpenAPI document The API reference on this site is generated from `https://tawked.com/openapi.json`, an OpenAPI 3.1 document with every endpoint, parameter, response example and error code. Feed it to a code generator, a request client, or an agent that reads specs directly. Tawked also publishes an [RFC 9727 API catalog](https://tawked.com/.well-known/api-catalog) at the well-known path. ## A prompt that works Paste this into your agent along with the MCP server or the `llms-full.txt` file: ```text theme={null} Integrate Tawked OTP verification into this project. Use the Tawked docs (MCP server or llms-full.txt) for every request shape and error code; do not guess. Rules from the docs: authenticate with a Bearer key from an environment variable, never in code. Call POST /v1/verify/start with `to`, `lang` and a `reference`, and send an Idempotency-Key per attempt. Call POST /v1/verify/check with the id and the code the user typed; branch on `verified`, then on `status`. Match errors on the `error` string, never on the HTTP status alone. Use a tk_test_ key until the application is approved. ``` Agents connected to the MCP server read the docs only. They cannot send messages or spend credits; that still needs your API key in your own code. # Cancel a verification Source: https://tawked.mintlify.app/api-reference/verify/cancel-a-verification https://tawked.com/openapi.json post /v1/verify/{id}/cancel Cancels a pending verification. A later `check` on it answers `{ "verified": false, "status": "canceled" }`. # Check a code Source: https://tawked.mintlify.app/api-reference/verify/check-a-code https://tawked.com/openapi.json post /v1/verify/check Verifies the code the end user typed against the id from /verify/start. # Get a verification Source: https://tawked.mintlify.app/api-reference/verify/get-a-verification https://tawked.com/openapi.json get /v1/verify/{id} Reads the current state of a verification — status, channel, destination, and how many attempts and resends remain — without consuming an attempt. # Resend a code Source: https://tawked.mintlify.app/api-reference/verify/resend-a-code https://tawked.com/openapi.json post /v1/verify/{id}/resend Sends a new code on the same id: the previous code stops working, attempts and the expiry reset, and the send is charged and rate-limited like any other — up to 3 resends per verification. # Start a verification Source: https://tawked.mintlify.app/api-reference/verify/start-a-verification https://tawked.com/openapi.json post /v1/verify/start Sends a one-time code to a destination and returns an id to check it against. # Get a message Source: https://tawked.mintlify.app/api-reference/whatsapp/get-a-message https://tawked.com/openapi.json get /v1/whatsapp/messages/{id} Reads a message's current status and timestamps. Statuses arrive from Meta within about a minute; `error` is set when the message failed. A message belongs to the key's application; any other id answers `404`. # List templates Source: https://tawked.mintlify.app/api-reference/whatsapp/list-templates https://tawked.com/openapi.json get /v1/whatsapp/templates Lists the templates of the number's WhatsApp Business Account as Meta holds them, with the placeholders each one expects, so your code can discover what to send without WhatsApp Manager. Cached for 5 minutes. # Send a template message Source: https://tawked.mintlify.app/api-reference/whatsapp/send-a-template-message https://tawked.com/openapi.json post /v1/whatsapp/messages Sends an approved template to a Saudi mobile through the number attached to the application, and answers `202` with an id to track it. The fee, if any, is for the send; a failed send refunds it. # Authentication Source: https://tawked.mintlify.app/authentication API keys, their modes and scopes, and the hardening options. Every request carries an API key. Send it as a Bearer token, or in the `x-api-key` header for clients that cannot set `Authorization`: ```bash theme={null} curl https://tawked.com/v1/verify/b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44 \ -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" ``` Keys are created in the dashboard under **Applications, then your application, then Keys**, and shown once. Store them in your secret manager, never in client-side code. ## Key modes The prefix tells you the mode; the API enforces it. | Prefix | Mode | What it can do | | ------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tk_live_` | Live | Production sends. Needs an approved application. | | `tk_test_` | Sandbox | Everything a live key does, with the [sandbox limits](/sandbox). Issued before approval. | | `tk_partner_` | Partner | Account-level key for platforms that provision applications for their merchants. Pass `application` in the body (or `?application=` on a GET) to say which application is sending. | A live key on a sandbox-only account, or a revoked key, answers `401 unauthorized`. A key that belongs to an application that is not approved yet answers `403 account_not_active`. ## Scopes A key is `full` (the default) or `check`. A check-scoped key may only call `check` and `GET /v1/verify/{id}`; `start`, `resend` and `cancel` answer `403 insufficient_scope`. Use it on a server that only ever confirms codes and never sends them. Partner keys are always full scope. ## Expiry and IP allowlists Set at creation time, checked right after the key is recognised and before anything else: * **Expiry**: once `expires_at` passes, every call answers `401 key_expired`. * **IP allowlist**: exact IPs and CIDR ranges, IPv4 and IPv6. A caller outside the list gets `403 ip_not_allowed`. Empty means every address. Rotate keys by creating the new one first, deploying it, then revoking the old one. There is no window where both must be in the same request. ## The order of checks Knowing the order helps you read an error. On every endpoint: 1. Per-key rate limit (`429 too_many_requests`), checked before authentication. 2. Key recognised, then expiry, IP allowlist and scope. 3. Account and application state (`403 account_not_active`). 4. Request shape (`400 invalid_json`, `400 invalid_request`), channel and destination (`422`). 5. Sandbox gates, hourly caps and the protection guard (`429`, `403`). 6. Balance (`402 insufficient_credits`). 7. The send itself (`502 send_failed`, refunded). The [Errors](/errors) page lists every code. # Destinations and messages Source: https://tawked.mintlify.app/destinations Accepted number formats, message language, and autofill-ready SMS. ## Accepted formats Tawked verifies Saudi mobile numbers only. Send `to` in any of these forms; it is stored and echoed back as E.164. | Example | Format | | ---------------- | ----------------------- | | `+966512345678` | E.164 (canonical) | | `966512345678` | Country code, no plus | | `00966512345678` | 00 international prefix | | `0512345678` | Local, with leading 0 | | `512345678` | Local, no leading 0 | Anything else answers `422 invalid_destination`. The deprecated field `phone` is still accepted as an alias for `to`, and `to` wins when both are present. ## Language `lang` picks the message language. Only the literal value `en` selects English; anything else, including a missing field, means Arabic. ## Sender identity The sender name and the message wording come from your reviewed application, never from the request. That is what the review is for: the person receiving the code sees your brand, and nobody can send under it with a stray key. ## Autofill-ready SMS Phones can read a code straight out of the SMS when the message ends with the right lines. Tawked adds them for you, after the human-readable text: ``` Your Flower Shop verification code is: 482913 FA+9qCX9VSu @shop.example.com #482913 ``` * **`@domain #code`**, the last line, is what iOS AutoFill and the WebOTP API read. The domain is derived from your application's reviewed website and is read-only; switch the line on or off per application. * **The app hash line** is for Google's SMS Retriever on Android. Set the 11-character hash per application. * The optional `autofill` field on `start` (`web`, `android` or `none`) picks which lines go out on that one send. Omit it to send every line the application has configured. The whole message must stay under 140 bytes for Android's SMS Retriever, and Arabic SMS is billed per 70-character segment. The domain line adds about 25 characters, which can turn one segment into two. The dashboard preview shows the segment count. Autofill lines are out of scope for WhatsApp: its message is a fixed Meta template with no room for either. # Errors Source: https://tawked.mintlify.app/errors Every error code the API returns, with its HTTP status and what to do about it. Errors are JSON with a stable, machine-readable `error` string. Match on the string, not on the HTTP status: two `429` codes and several `403` codes mean different things. Some errors add fields (`status`, `message`, `balance_halalas`, `price_halalas`, `provider_code`, `fields`). ```json theme={null} { "error": "insufficient_credits", "message": "Insufficient credits", "balance_halalas": 0, "price_halalas": 9 } ``` Every response carries an `X-Request-Id` header. Quote it when you contact support. ## Shared by every endpoint Checked in this order, before the endpoint's own logic. | HTTP | `error` | Meaning | | ---- | -------------------- | ------------------------------------------------------------------------------------------------------------- | | 429 | `too_many_requests` | Per-key rate limit exceeded (120 requests per minute, fixed 60-second window). Checked before authentication. | | 401 | `unauthorized` | Missing, unknown, revoked or mode-mismatched key, or an unknown or suspended partner. | | 401 | `key_expired` | The key's `expires_at` has passed. Checked right after the key is recognised. | | 403 | `ip_not_allowed` | The caller's IP is outside the key's `ip_allowlist`. | | 403 | `insufficient_scope` | A check-scoped key called anything but `check` or `GET /v1/verify/{id}`. | | 400 | `service_required` | A partner key with no `application` (or `service`) in the request. | | 404 | `service_not_found` | A partner key, but `application` does not match any of that partner's provisioned applications. | | 403 | `account_not_active` | The application is not approved yet, the account is banned, or a partner disabled the application. | | 400 | `invalid_json` | The request body is not valid JSON. | | 400 | `invalid_request` | A required field is missing: `to` on `start`, `id` or `code` on `check`. | | 500 | `internal_error` | An unhandled server error. Quote the `X-Request-Id`. | ## Verify | HTTP | `error` | Endpoints | Meaning | | ---- | -------------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------- | | 422 | `unsupported_channel` | start | Unknown channel, or a known channel that is not live over this API. | | 422 | `sandbox_unsupported_channel` | start | A sandbox key on a channel that cannot carry the `[TEST]` stamp (WhatsApp). | | 422 | `invalid_destination` | start | Not an accepted Saudi mobile format. See [Destinations](/destinations). | | 422 | `sandbox_unverified_destination` | start | Sandbox key, and the destination is not on the account's proven list. | | 429 | `sandbox_quota_exceeded` | start | The application's lifetime sandbox cap of 10 sends is reached. | | 429 | `rate_limited` | start, resend | The per-destination hourly cap is reached. Same status as `too_many_requests`, different meaning. | | 429 | `ip_rate_limited` | start, resend | The `client_ip` you passed sent more starts this hour than `max_per_ip_per_hour` allows. | | 429 | `spend_cap_reached` | start, resend | Today's charges plus this send would pass `daily_spend_cap_halalas`. | | 403 | `application_paused` | start, resend | Auto-paused by the protection guard after a spike in new destinations. The owner resumes it from the dashboard. | | 402 | `insufficient_credits` | start, resend | The prepaid balance is below the channel price. Nothing was sent. | | 502 | `send_failed` | start, resend | The delivery provider failed. The charge is refunded automatically; retry with the same `Idempotency-Key`. | | 404 | `not_found` | check, status, resend, cancel | Unknown id, or an id that belongs to another account or application. | | 409 | `not_resendable` | resend | Already verified, failed, expired or canceled. The body carries the current `status`. | | 409 | `not_cancelable` | cancel | Already in a terminal state. The body carries the current `status`. | | 429 | `resend_limit_reached` | resend | This verification already used its 3 resends. | ## WhatsApp After the shared checks, in this order. | HTTP | `error` | Meaning | | ---- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 403 | `live_key_required` | A sandbox key. WhatsApp has no sandbox; use the application's live key. | | 403 | `whatsapp_not_enabled` | WhatsApp is turned off for this application. Contact support if that is a mistake. | | 409 | `no_whatsapp_number` | No WhatsApp number is connected to this application yet. Connect one from the application's WhatsApp tab. | | 409 | `number_disconnected` | The number is attached but Meta does not report it as connected right now. The dashboard shows the number's health. | | 422 | `invalid_destination` | Not an accepted Saudi mobile format. | | 422 | `unknown_template` | No template with that name (and language, when given) on the number's WhatsApp Business Account. `GET /v1/whatsapp/templates` lists what exists. | | 422 | `template_not_approved` | The template exists but Meta has not approved it. `message` carries Meta's status. | | 422 | `template_category_not_allowed` | An authentication template. One-time codes go through Verify. | | 422 | `unsupported_template` | The template needs a media header, a non-text button parameter, or mixes named and positional placeholders. `message` says which. | | 422 | `invalid_params` | A placeholder value is missing, empty, unexpected, too long or contains a control character, or the template exists in several languages and `lang` was not given. `message` names the placeholder. | | 402 | `insufficient_credits` | The balance is below the WhatsApp message fee. The body adds `balance_halalas` and `price_halalas`. | | 502 | `send_failed` | Meta refused the send or is unreachable. The body adds `provider_code` and `message`; the fee is refunded. | | 503 | `templates_unavailable` | Meta did not answer the read of the template list. Nothing was sent or charged; retry shortly. | # Tawked developer docs Source: https://tawked.mintlify.app/index OTP verification and WhatsApp messages for Saudi mobile numbers, in a few HTTP calls. Tawked sends a one-time code to a Saudi mobile number and checks the code the person typed. Two calls, one prepaid balance, billed only per delivered code. Approved applications can also send WhatsApp template messages through their own number. Start a verification and check a code in five minutes, with a sandbox key. Every endpoint with an interactive playground, generated from the live OpenAPI document. Every error code the API returns, its HTTP status, and what to do about it. Send approved templates through the number connected to your application. ## The shape of the API | | | | -------------- | ----------------------------------------------------------------------------------- | | Base URL | `https://tawked.com` | | Authentication | `Authorization: Bearer ` or `x-api-key: ` | | Format | JSON in, JSON out, UTF-8 | | Rate limit | 120 requests per minute per key | | Price | SAR 0.09 per delivered SMS code, prepaid; failed sends are not charged | | Request id | Every response carries an `X-Request-Id` header. Quote it when you contact support. | Both `/v1/...` and `/api/v1/...` answer identically. New integrations should use `/v1/...`. ## Machine-readable The same reference is published for tools and agents: an [OpenAPI 3.1 document](https://tawked.com/openapi.json), an [llms.txt index](https://tawked.com/llms.txt) and the [full reference as Markdown](https://tawked.com/llms-full.txt). All three are generated from one source, so they never disagree with these pages. # Quickstart Source: https://tawked.mintlify.app/quickstart Send a code and verify it, end to end, with a sandbox key. Sign in to the [dashboard](https://tawked.com/en/login), finish onboarding, and open **Applications**. A sandbox key (`tk_test_...`) is issued the moment onboarding completes, before your application is reviewed. Copy it once: keys are shown a single time. A sandbox key sends real messages, but only to numbers you have proven you own in the dashboard, and only 10 times per application. The [Sandbox](/sandbox) page has the details. Send a code to the destination. Any accepted Saudi mobile format works; `lang` picks the message language (Arabic unless it is exactly `en`). ```bash cURL theme={null} curl -X POST https://tawked.com/v1/verify/start \ -H "Authorization: Bearer tk_test_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "to": "0551234567", "lang": "ar" }' ``` ```javascript Node.js theme={null} const res = await fetch('https://tawked.com/v1/verify/start', { method: 'POST', headers: { Authorization: `Bearer ${process.env.TAWKED_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to: '0551234567', lang: 'ar' }), }); const { id } = await res.json(); // keep the id for the check ``` ```php PHP theme={null} $res = Http::withToken(env('TAWKED_KEY')) ->post('https://tawked.com/v1/verify/start', ['to' => '0551234567', 'lang' => 'ar']); $id = $res->json('id'); // keep the id for the check ``` ```python Python theme={null} import os, requests res = requests.post( "https://tawked.com/v1/verify/start", headers={"Authorization": f"Bearer {os.environ['TAWKED_KEY']}"}, json={"to": "0551234567", "lang": "ar"}, ) verification_id = res.json()["id"] # keep the id for the check ``` The answer is `201` with the id you will check against and when the code expires: ```json theme={null} { "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44", "status": "pending", "expires_at": "2026-09-01T12:34:56.789Z" } ``` Post the id and the code the person typed. The code is compared exactly as sent: never trimmed, never normalised. ```bash theme={null} curl -X POST https://tawked.com/v1/verify/check \ -H "Authorization: Bearer tk_test_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44", "code": "482913" }' ``` Every outcome is a `200`. Read `verified` first, then `status` for the reason: ```json theme={null} { "verified": true, "status": "verified" } ``` ```json theme={null} { "verified": false, "status": "invalid_code", "attempts_remaining": 2 } ``` `status` is one of `verified`, `invalid_code`, `too_many_attempts`, `failed`, `expired` or `canceled`. An unknown id is the one exception: `404 { "error": "not_found" }`. A verified request stays verified and ignores later codes. Once your application is approved, create a live key (`tk_live_...`) on the same **Keys** page and swap it in. The endpoints and shapes are identical; the `[TEST]` stamp disappears and the sandbox caps no longer apply. ## Defaults you can tune Each application sets its own verification settings in the dashboard. The defaults, and their bounds: | Setting | Default | Bounds | | ------------------------------ | ----------- | --------- | | Code length | 6 digits | 4 to 8 | | Code lifetime | 300 seconds | 60 to 900 | | Attempts per code | 3 | 1 to 5 | | Sends per destination per hour | 5 | 1 to 10 | | Resends per verification | 3 | fixed | ## What next Key modes, scopes, expiry and IP allowlists. Idempotency keys, rate limits and the protection guard. Get told when a verification is verified, fails or expires. A new code on the same id, or a clean cancel. # Reliability and limits Source: https://tawked.mintlify.app/reliability Idempotency keys, rate limits, per-destination caps, and the protection guard. ## Idempotency Send an `Idempotency-Key` header on `start` (and on WhatsApp sends) to make retries safe. The key is 1 to 128 printable ASCII characters that you generate, scoped to your API key. Replaying it returns the original `201` unchanged, with the response header `Idempotent-Replayed: true`; nothing is sent or charged again. A send that fails releases the key so a retry can go through. ```bash theme={null} curl -X POST https://tawked.com/v1/verify/start \ -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" \ -H "Idempotency-Key: signup-8f1c2-attempt" \ -H "Content-Type: application/json" \ -d '{ "to": "0551234567", "reference": "signup-8f1c2" }' ``` Use one key per business event, and retry a `502` or a network error with the same key. ## Your own reference `reference` (up to 64 printable characters) is echoed on `start` and on `GET /v1/verify/{id}`, and is searchable in the dashboard's verifications list. Put your order id or session id there. ## Rate limits | Limit | Value | Error | | ------------------------ | --------------------------------------------------------------------------- | -------------------------- | | Requests per key | 120 per minute, fixed 60-second window | `429 too_many_requests` | | Sends per destination | 5 per hour by default, per account, across applications, channels and modes | `429 rate_limited` | | Resends per verification | 3 | `429 resend_limit_reached` | The two `429` codes share a status and differ in the `error` string. The per-key limit fails open if the limiter itself is unavailable. ## The protection guard Three per-application settings stop a runaway integration or an attacker from spending your balance. All are evaluated on `start` and `resend`, and skipped in the sandbox. * **Daily spend cap** (`daily_spend_cap_halalas`, off by default): today's charges plus this send would pass the cap, so the send answers `429 spend_cap_reached`. Days are Riyadh calendar days. * **Per-IP cap** (`max_per_ip_per_hour`, default 20): only checked when you pass `client_ip` on `start`. The address is never inferred from the request. Over the cap answers `429 ip_rate_limited`. * **New-destination pumping** (`max_new_destinations_per_hour`, default 300): crossing it auto-pauses the application. Every call then answers `403 application_paused` until the owner resumes it from the dashboard. The pause is audit-logged and the owner is notified. Pass `client_ip` whenever the verification is started on behalf of an end user's request. It is the one signal that lets Tawked tell many users apart from one abuser. ## Verification lifecycle ``` start ──▶ pending ──▶ verified │ ├──▶ failed (attempts exhausted, or the send failed) ├──▶ expired (the code lifetime passed) └──▶ canceled (you called cancel) ``` `resend` keeps the id and issues a new code: the old code stops working, attempts and expiry reset, and the send is charged and rate-limited like any other. A new `start` never invalidates an earlier code. Read the current state at any time with `GET /v1/verify/{id}` without consuming an attempt. # Sandbox Source: https://tawked.mintlify.app/sandbox Test against the real API, with real messages, before your application is approved. A sandbox key (`tk_test_...`) is issued the moment onboarding completes. It calls the exact same endpoints with the exact same request and response shapes as a live key, so the code you write against it is the code you ship. ## What is different * **Proven destinations only.** Messages go only to numbers the account has proven it owns, up to 3 per account. Add one in the dashboard with an OTP round-trip. Any other destination answers `422 sandbox_unverified_destination`. * **A visible stamp.** Every message carries an unremovable `[TEST]` / `[تجريبي]` stamp, before any other line. * **A lifetime cap.** 10 sends per application, charged from the same prepaid balance as live sends, so testing cannot consume the whole welcome credit. The cap answers `429 sandbox_quota_exceeded`. * **SMS only.** WhatsApp cannot be used in the sandbox at all. The Verify API refuses it with `422 sandbox_unsupported_channel`, and the WhatsApp endpoints with `403 live_key_required`. ## A typical test In the dashboard, add your own mobile as a sandbox destination and type the code it receives. Follow the [Quickstart](/quickstart) with the sandbox key. Read the code off your phone and check it. Type a wrong code to see `invalid_code` and `attempts_remaining` count down. Call [resend](/api-reference/verify/resend-a-code) and [cancel](/api-reference/verify/cancel-a-verification). Wait out the lifetime to see `expired`. Unit tests should not hit the sandbox: every send is a real SMS and counts against the cap. Record the documented responses as fixtures instead; the [API reference](/api-reference/verify/start-a-verification) carries every example body. # Webhooks Source: https://tawked.mintlify.app/webhooks Signed events when a verification is verified, fails or expires, and when a WhatsApp message changes status. Webhooks are configured per application on its **Webhooks** tab: a URL and a secret, revealed on demand and rotatable. Every delivery is signed, retried with backoff on anything but a `2xx`, and logged attempt by attempt in the dashboard. ## Events | Event | Fires from | | --------------------------------------------------------------------- | -------------------------------------------------------------------- | | `verification.verified` | `check` | | `verification.failed` | `check` | | `verification.expired` | The expiry sweep, or any `check` or `GET` that discovers the expiry | | `message.sent`, `message.delivered`, `message.read`, `message.failed` | The WhatsApp status sync, within about a minute of Meta reporting it | ## The envelope ```json theme={null} { "event": "verification.verified", "data": { "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44", "status": "verified", "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f", "reference": "order-42", "channel": "sms", "to": "+966551234567", "mode": "live", "attempts": 1, "expires_at": "2026-09-01T12:34:56.789Z", "verified_at": "2026-09-01T12:31:02.145Z", "created_at": "2026-09-01T12:29:56.789Z" }, "sent_at": "1788265862201" } ``` WhatsApp events use the same envelope with the message's fields under `data`, plus `occurred_at` for the time of the status the event announces, and `error` set only on `message.failed`. See [WhatsApp messages](/whatsapp/messages#statuses-and-webhooks). ## Verifying the signature Every delivery carries three headers: | Header | Value | | ------------------ | -------------------------------------------------------------- | | `tawked-timestamp` | Unix time in milliseconds | | `tawked-signature` | Hex HMAC-SHA256 of `"{timestamp}.{raw body}"` with your secret | | `tawked-event` | The event name | Compute the HMAC over the **raw** request body, before any JSON parsing, and compare it in constant time: ```javascript Node.js theme={null} import { createHmac, timingSafeEqual } from 'node:crypto'; export function verify(rawBody, headers, secret) { const expected = createHmac('sha256', secret) .update(`${headers['tawked-timestamp']}.${rawBody}`) .digest('hex'); const given = headers['tawked-signature'] ?? ''; return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected)); } ``` ```php PHP theme={null} $expected = hash_hmac('sha256', $request->header('tawked-timestamp').'.'.$request->getContent(), $secret); abort_unless(hash_equals($expected, (string) $request->header('tawked-signature')), 401); ``` ```python Python theme={null} import hmac, hashlib def verify(raw_body: bytes, headers: dict, secret: str) -> bool: msg = f"{headers['tawked-timestamp']}.".encode() + raw_body expected = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, headers.get("tawked-signature", "")) ``` Reject deliveries whose timestamp is older than a few minutes to close the replay window, and answer `2xx` quickly. Do your own work after acknowledging, or the delivery is retried. Partner-provisioned applications receive partner webhooks instead; client webhooks are not configurable on them yet. # Sending messages Source: https://tawked.mintlify.app/whatsapp/messages Send a template, read its status, and receive status webhooks. ## Discover the template Templates are listed as Meta holds them, with the placeholders each one expects, so your code can discover what to send without opening WhatsApp Manager. The list is cached for five minutes. ```bash theme={null} curl https://tawked.com/v1/whatsapp/templates \ -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" ``` ```json theme={null} { "data": [ { "name": "order_received_v1", "language": "ar", "category": "UTILITY", "status": "APPROVED", "placeholders": { "kind": "named", "header": [], "body": ["name", "order"], "buttons": [{ "index": 0, "type": "url" }] } } ] } ``` ## Send Every placeholder the template declares is required, extra keys are refused, and values are strings of 1 to 1024 characters without newlines or control characters. Give `lang` when the template exists in more than one language. ```bash theme={null} curl -X POST https://tawked.com/v1/whatsapp/messages \ -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042-received" \ -d '{ "to": "0550000000", "template": "order_received_v1", "lang": "ar", "params": { "name": "أحمد", "order": "1042" }, "buttons": [{ "index": 0, "parameter": "1042" }], "reference": "order-1042" }' ``` The answer is `202 Accepted`. Meta has taken the message; the status arrives afterwards. ```json theme={null} { "id": "0f4c9c0e-6d2b-4b8a-9c3e-7a1d2e3f4a5b", "status": "accepted", "to": "+966550000000", "template": "order_received_v1", "lang": "ar", "reference": "order-1042", "price_halalas": 0, "created_at": "2026-09-06T12:00:00+03:00" } ``` | Field | Notes | | ----------- | -------------------------------------------------------------------------------------------- | | `to` | A Saudi mobile in any [accepted format](/destinations), stored as E.164. | | `template` | The name exactly as WhatsApp Manager lists it. | | `lang` | Meta's language code (`ar`, `en`, `en_US`). Required when the template has several. | | `params` | Placeholder values, keyed by name for a named template or `"1"`, `"2"` for a positional one. | | `header` | `{ "text": "..." }` when the template has a text header placeholder; refused otherwise. | | `buttons` | `[{ "index": 0, "parameter": "..." }]`, one entry per URL button with a placeholder. | | `reference` | Your own reference, 1 to 64 printable characters, echoed back and searchable in the console. | Send an `Idempotency-Key` per business event and retry a `502` or a network error with the same key. A replay returns the original `202` with `Idempotent-Replayed: true` and sends nothing. See [Reliability](/reliability#idempotency). ## Statuses and webhooks `accepted` then `sent`, `delivered`, `read`, or `failed` from any of the first three. Statuses arrive from Meta within about a minute, are applied in order and never go backwards. A message stops being tracked 48 hours after the send and keeps its last status. Read the current state with `GET /v1/whatsapp/messages/{id}`: ```json theme={null} { "id": "0f4c9c0e-6d2b-4b8a-9c3e-7a1d2e3f4a5b", "status": "failed", "to": "+966550000000", "template": "order_received_v1", "lang": "ar", "reference": "order-1042", "price_halalas": 0, "error": { "code": "131026", "message": "Message undeliverable" }, "created_at": "2026-09-06T12:00:00+03:00", "sent_at": null, "delivered_at": null, "read_at": null, "failed_at": "2026-09-06T12:00:03+03:00" } ``` Or receive one webhook per status change, at the application's webhook URL, signed and retried exactly like `verification.*` events. Only the status reached is announced: a message that jumps straight to `read` gets one `message.read`. `to` is masked in webhooks the way the console shows it, so keep your own copy of the destination next to the returned `id` or your `reference`. ```json theme={null} { "event": "message.delivered", "data": { "id": "0f4c9c0e-6d2b-4b8a-9c3e-7a1d2e3f4a5b", "status": "delivered", "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f", "to": "+9665•• ••• 000", "template": "order_received_v1", "lang": "ar", "reference": "order-1042", "price_halalas": 0, "error": null, "occurred_at": "2026-09-06T09:00:04.512Z", "created_at": "2026-09-06T09:00:01.087Z" }, "sent_at": "1788685205201" } ``` Signature verification is the same as for verification events: see [Webhooks](/webhooks#verifying-the-signature). # WhatsApp overview Source: https://tawked.mintlify.app/whatsapp/overview Send approved WhatsApp templates through your own number, with the application's live key. An approved application can connect its WhatsApp Business Account from the dashboard and send approved template messages through the API. Tawked validates the template against the number's live template list, charges the fee, sends through Meta on your behalf, records the message, mirrors it into your Tawked Chat inbox, and tracks delivery and read. Your code never holds a Meta token. One-time codes are not sent here. Authentication templates are refused with `template_category_not_allowed`; use the [Verify API](/quickstart) for codes. This surface is for utility and marketing templates: order updates, reminders, receipts. ## Getting started Dashboard, then **Applications**, your approved application, **WhatsApp**, **Connect WhatsApp**. Meta opens a window where you pick or create the business and the number. Tawked registers it and sets up the inbox. You keep full access in WhatsApp Manager and pay Meta directly for conversations. On the application's **Templates** page, or in WhatsApp Manager. Meta reviews it, usually within minutes; only an approved template can be sent. WhatsApp sends need a `tk_live_` key of the application the number is connected to. A `tk_test_` key is refused with `403 live_key_required`; there is no WhatsApp sandbox. [`GET /v1/whatsapp/templates`](/api-reference/whatsapp/list-templates) shows every template with the placeholders it expects. [`POST /v1/whatsapp/messages`](/api-reference/whatsapp/send-a-template-message) sends one and answers `202` with an id. Read the status back with [`GET /v1/whatsapp/messages/{id}`](/api-reference/whatsapp/get-a-message), or receive `message.*` [webhooks](/webhooks). ## What the API carries today * Body placeholders, named (`{{name}}`) or positional (`{{1}}`), never mixed. * A text header placeholder. * The dynamic suffix of URL buttons. Media headers and buttons that take a non-text parameter (copy code, flow, catalog, carousel, limited-time offer) are refused with `422 unsupported_template`. ## Billing The fee, if any, is for the send. Delivery and read cost nothing more, and a failed send refunds the fee once. Meta bills you for conversations separately, on your own WhatsApp Business Account.