Skip to content

For developers

Two calls between you and a verified user.

Send the code with verify/start and check it with verify/check.

Tawked Verify is an OTP verification API for Saudi numbers.

The full developer portalGuides, an interactive reference you can call from the page, every page as Markdown, and an MCP server for your coding agent.Open docs.tawked.com

Quickstart

  1. 01

    Create an API key

    Create a tk_test_ key in your application's Keys tab. It works before application approval.

  2. 02

    Send the code

    POST to /v1/verify/start with a destination, and you get back an id and an expiry.

  3. 03

    Check the code

    POST to /v1/verify/check with the same id and the code the user typed.

Channels

Verification codes over WhatsApp are coming soon; SMS works today. WhatsApp template messages, such as order updates and reminders, have their own endpoint below, from your own connected number.

Channelchannel valueAccepted destination formatsNotesPrice
SMSsms+966512345678, 966512345678, 00966512345678, 0512345678, 512345678Any local format is normalized to +9665XXXXXXXX automatically before sending.0.09 SAR per code sent

Authentication

Pass your key in the Authorization header as a Bearer token, or in the x-api-key header if that is not possible. When both are present, Authorization wins.

Live key

Authorization: Bearer tk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Test key

Authorization: Bearer tk_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Partner key

Authorization: Bearer tk_partner_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

A partner key names the application in the application field of the request body, or in service, its legacy name, since one partner key covers every one of its stores.

Key scopes and hardening

Set when you create the key in the console. All optional.

  • `expires_at` (optional): once it passes, every call with that key answers `401 key_expired`, checked right after the key is recognised, before anything else.
  • `ip_allowlist` (optional): exact IPs and/or CIDR ranges, IPv4 and IPv6. Empty or absent allows every address; a caller outside the list gets `403 ip_not_allowed`.
  • Access: a key has full access, which covers every product and any product added later, or custom access, which covers only what it was granted, product by product. Verify: none, check only, or send and check. Notifications: none, read only, or send. Whatever its access, a key is a server-side secret: never put one in a browser or a mobile app.
  • The scopes behind those levels: `verify:check` calls `POST /v1/verify/check` and `GET /v1/verify/{id}`. `verify:send` calls `POST /v1/verify/start`, `POST /v1/verify/{id}/resend` and `POST /v1/verify/{id}/cancel`, and includes `verify:check`. `notifications:read` calls `GET /v1/whatsapp/messages/{id}`, `GET /v1/whatsapp/templates` and `GET /v1/whatsapp/templates/{template_id}`. `notifications:send` calls `POST /v1/whatsapp/messages`, and includes `notifications:read`. One more scope is not a level of that page: `notifications:templates` calls `POST /v1/whatsapp/templates`, `PATCH /v1/whatsapp/templates/{template_id}` and `DELETE /v1/whatsapp/templates/{template_id}`, and includes `notifications:read`. No key made on the API keys page holds it, full access included: those three calls answer such a key `403 insufficient_scope`. A connected app's key holds it.
  • A call outside the key's access answers `403` with the body `{"error":"insufficient_scope"}`, and the response header names the scope the call needs: `WWW-Authenticate: Bearer error="insufficient_scope", scope="verify:send"`. It is judged right after the key is recognised: after `unauthorized`, `key_expired` and `ip_not_allowed`, before `account_not_active` and the request's shape.
  • A check-only key created before access was set product by product works as it always did: it holds `verify:check` and `notifications:read`, so it checks codes and reads statuses and templates on both products, and sends on neither.
  • A test key cannot be granted Notifications: WhatsApp has no sandbox, and `/v1/whatsapp/*` refuses every test key.
  • Email: none, read only, or send. `email:read` calls `GET /v1/email/messages/{id}`. `email:send` calls `POST /v1/email/messages`, and includes `email:read`. A test key cannot be granted Email: there is no e-mail sandbox, and `/v1/email/*` refuses every test key.
  • Set at creation on the application's API keys page in the client console (Create a key). A key's access cannot be changed afterwards: create a new key with the access you need. A partner rotating its key can set an expiry and an IP allowlist too; a partner key always has full access and cannot be narrowed.

Sandbox

Create a tk_test_ key in the dashboard before application approval. It uses the same endpoints and request formats as a live key.

tk_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
  • Created by you, before application approval: on the application's API keys page (Create a key, type Test), or made for you by the first send from Verify → Test in the console.
  • Calls the exact same endpoints with the exact same request/response shapes as a live key.
  • Sends real messages, but only to one number: the phone the account owner verified at signup. No other destination can be added.
  • Every message carries an unremovable [TEST]/[تجريبي] stamp.
  • Charged like any other send, from the same prepaid balance, and capped at 10 lifetime sends per application so testing cannot consume the whole welcome credit.
  • WhatsApp has no sandbox: `/v1/whatsapp/*` refuses every test key, with `403 live_key_required` (or `403 insufficient_scope` for a test key with custom access, which never holds Notifications).

Autofill-ready messages

The autofill domain comes from your reviewed application website. Add an Android app hash for Google SMS Retriever, alongside iOS AutoFill and WebOTP support.

  • The domain is never typed: `autofill_domain` is derived from the application's reviewed website (its registrable domain, so subdomains are covered) and is read-only in every response. `autofill_domain_enabled` switches its line on or off; `android_app_hash` (11 characters) is set per application, from the dashboard, or through the partner API on create and on the verify-settings patch.
  • The `@domain #code` line adds about 25 characters. Arabic SMS is billed per 70-character segment, so the line can turn one segment into two: the console preview shows the segment count, and the switch exists for exactly that reason.
  • The SMS gains, after the human-readable text: the Android app-hash line (when a hash is set and Android is targeted), then `@domain #code` as the very last line (when a domain is set and the web is targeted).
  • iOS AutoFill and the WebOTP API read only the last line of the message, which is why the domain line always comes last, after the app hash.
  • Google's SMS Retriever only needs the hash present anywhere in the message, and the whole message under 140 bytes. GSM-7 text is 1 byte per character; anything outside that alphabet (most Arabic) is 2 bytes per character.
  • The optional `autofill` field on `start` (`web`, `android` or `none`) picks which line(s) go out on that one send; omit it to send every line the application has configured.
  • The sandbox stamp, when present, always comes first, before any autofill line.
  • Out of scope for WhatsApp: its message is a fixed Meta template with no room for either line.

Example message, under 140 bytes

Your Flower Shop verification code is: 482913
FA+9qCX9VSu
@shop.example.com #482913

Endpoints

POST/v1/verify/start

Start a verification

Sends a one-time code to a destination and returns an id to check it against.

Parameters

tostringRequired

The destination. Any of the accepted Saudi mobile formats below.

phonestringOptional

Deprecated alias for `to`, kept for pre-multichannel integrations. Only used when `to` is absent or blank; `to` always wins when both are present. New integrations should use `to`.

channelstringOptional

Delivery channel. Defaults to "sms" when absent or blank. "whatsapp" answers `unsupported_channel` until Tawked opens WhatsApp for codes to every account.

langstringOptional

Message language. Only the literal value "en" selects English; anything else (including missing) defaults to Arabic.

applicationstringPartner key

Partner keys only: which of your provisioned applications is sending. The legacy field name `service` is accepted identically.

referencestringOptional

Your own reference (order id, request id, …), up to 64 printable characters. Echoed back on `start` and on `GET /v1/verify/{id}`, and searchable in the dashboard's verifications list.

client_ipstringOptional

The end user's IP address. Enables the per-IP protection guard for this application when one is configured. Never inferred from the request, always explicit.

autofillstringOptional

`web`, `android` or `none`: which autofill line(s) to include on this send. `web` sends only the `@domain #code` line, `android` only the app-hash line, `none` sends neither. Omit it to send every line the application has configured.

Headers

Idempotency-KeystringOptional

A 1–128 printable-ASCII-character key you generate, scoped to your API key (and, for a partner key, to the named application too). Replaying the same key returns the original `201` response unchanged, with the response header `Idempotent-Replayed: true`, and nothing is sent or charged again. A send that fails releases the key so a retry can go through.

Response

201Started
{
    "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44",
    "status": "pending",
    "expires_at": "2026-09-01T12:34:56.789Z"
}

Request example

curl -X POST https://tawked.com/v1/verify/start \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "0551234567",
    "channel": "sms",
    "lang": "ar"
}'
POST/v1/verify/check

Check a code

Verifies the code the end user typed against the id from /verify/start.

Parameters

idstringRequired

The id returned by /verify/start.

codestringRequired

The code the end user typed. Compared exactly as sent, never trimmed or normalized, so a stray space will not match.

applicationstringPartner key

Partner keys only, same meaning as on /verify/start. A key can only check a code started under the same application.

Response

200Verified
{
    "verified": true,
    "status": "verified"
}
200Wrong code
{
    "verified": false,
    "status": "invalid_code",
    "attempts_remaining": 2
}
200Attempts exhausted
{
    "verified": false,
    "status": "too_many_attempts",
    "attempts_remaining": 0
}
200Send failed / prior attempts exhausted
{
    "verified": false,
    "status": "failed"
}
200Expired
{
    "verified": false,
    "status": "expired"
}
200Canceled
{
    "verified": false,
    "status": "canceled"
}
404Unknown id
{
    "error": "not_found"
}

Request example

curl -X POST https://tawked.com/v1/verify/check \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44",
    "code": "482913"
}'
GET/v1/verify/{id}

Get a verification

Reads the current state of a verification, meaning its status, channel, destination, and the attempts and resends left, without consuming an attempt.

Parameters

applicationstringPartner key

Partner keys only: passed as a query parameter (`?application=…`) since a GET request has no body. Same meaning as on `start`; the legacy name `service` is accepted identically.

Response

200Current state
{
    "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44",
    "status": "pending",
    "channel": "sms",
    "to": "+966551234567",
    "reference": "order-42",
    "attempts_remaining": 3,
    "resends_remaining": 3,
    "expires_at": "2026-09-01T12:34:56.789Z",
    "verified_at": null,
    "created_at": "2026-09-01T12:29:56.789Z"
}
404Unknown id
{
    "error": "not_found"
}

Request example

curl https://tawked.com/v1/verify/b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44 \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx"
POST/v1/verify/{id}/resend

Resend a code

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.

Parameters

applicationstringPartner key

Partner keys only, same meaning as on `start`.

Response

200New code sent
{
    "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44",
    "status": "pending",
    "expires_at": "2026-09-01T12:39:56.789Z",
    "resends_remaining": 2
}
409Already in a terminal state
{
    "error": "not_resendable",
    "status": "verified"
}
429Resend limit reached
{
    "error": "resend_limit_reached"
}

Request example

curl -X POST https://tawked.com/v1/verify/b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44/resend \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx"
POST/v1/verify/{id}/cancel

Cancel a verification

Cancels a pending verification. A later `check` on it answers `{ "verified": false, "status": "canceled" }`.

Parameters

applicationstringPartner key

Partner keys only, same meaning as on `start`.

Response

200Canceled
{
    "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44",
    "status": "canceled"
}
409Already in a terminal state
{
    "error": "not_cancelable",
    "status": "verified"
}

Request example

curl -X POST https://tawked.com/v1/verify/b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44/cancel \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx"

WhatsApp messages

Send an approved WhatsApp template through your own number, connected to your application, with the application's live key. 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 the Tawked Chat thread, and tracks delivery and read. Your code never holds a Meta token. An application in review or approved connects its WhatsApp Business Account from the dashboard.

Getting started

Four steps from nothing to the first message, three of them in the dashboard.

  1. 01

    Connect your number

    Dashboard → Applications → your application, in review or approved → Notifications → Connect WhatsApp; the owner or an admin connects it. Meta opens a window where you pick, or create, the business and the number; Tawked registers it and sets up the inbox here. You keep full access in WhatsApp Manager and pay Meta directly for messages.

  2. 02

    Create a template

    On the application's Templates page, from Meta's template library, or in WhatsApp Manager. Meta reviews it in up to 24 hours; only an approved template can be sent. Utility, marketing and authentication templates all work here; an authentication template sends a one-time code from your own number.

  3. 03

    Use the application's live key

    Dashboard → Applications → your application → Keys. WhatsApp sends need a tk_live_ key of the application the number is connected to, with full access or with Notifications set to send; a tk_test_ key is refused, there is no WhatsApp sandbox.

  4. 04

    List, then send

    GET /v1/whatsapp/templates shows every template with the placeholders it expects. POST /v1/whatsapp/messages with to, template and params sends one; the reply is a 202 with an id. Read the status back with GET /v1/whatsapp/messages/{id}, or receive message.* webhooks.

POST/v1/whatsapp/messages

Send a template message

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.

Parameters

tostringRequired

The destination: a Saudi mobile in any of the accepted formats, stored as E.164.

templatestringRequired

The template name exactly as WhatsApp Manager lists it, 1 to 512 characters. It must be approved on the number's WhatsApp Business Account.

langstringOptional

The template language as Meta lists it (`ar`, `en`, `en_US`). Required when the template exists in more than one language.

paramsobjectOptional

Placeholder values for the body: the keys are the placeholder names of a named template (`{{name}}`), or `"1"`, `"2"` for a positional one (`{{1}}`). Every placeholder is required, extra keys are refused, values are strings of 1 to 1024 characters. An authentication template takes exactly `{ "code": "…" }`, 4 to 15 letters or digits, which fills its text and its button. `GET /v1/whatsapp/templates` lists the placeholders of each template.

headerobjectOptional

`{ "text": "…" }`: the value of the text header's placeholder. Required when the template has one, refused otherwise; no newlines. For a template that opens with an image, `{ "image": { "link": "https://…" } }`: the image's public https link, at most 2048 characters, a JPG or PNG of at most 5 MB that Meta fetches when it sends; required for such a template, refused for any other. Video, document and location headers are not supported yet.

buttonsarrayOptional

`[{ "index": 0, "parameter": "…" }]`: the dynamic suffix of each URL button, one entry per button that has a placeholder; `index` is the button's position among the template's buttons. Refused for an authentication template: `params.code` fills its button.

referencestringOptional

Your own reference (order id, event id), 1 to 64 printable characters. Echoed back, and searchable in the Messages log of the console.

Headers

Idempotency-KeystringOptional

A 1 to 128 printable-ASCII-character key you generate, scoped to your API key. Replaying the same key returns the original `202` response unchanged, with the response header `Idempotent-Replayed: true`: nothing is sent or charged again. A send that fails with `502` releases the key. Use one key per business event (`order-1042-received`) and retry a `502` or a network error with the same key.

Response

202Accepted
{
    "id": "0f4c9c0e-6d2b-4b8a-9c3e-7a1d2e3f4a5b",
    "status": "accepted",
    "to": "+966550000000",
    "template": "order_received",
    "lang": "ar",
    "reference": "order-30517",
    "price_halalas": 0,
    "created_at": "2026-09-06T12:00:00+03:00"
}
422Unknown template
{
    "error": "unknown_template"
}
402Insufficient credits
{
    "error": "insufficient_credits",
    "message": "Insufficient credits",
    "balance_halalas": 0,
    "price_halalas": 9
}

Request example

curl -X POST https://tawked.com/v1/whatsapp/messages \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-30517-received" \
  -d '{
    "to": "0550000000",
    "template": "order_received",
    "lang": "ar",
    "params": {
        "customer_name": "سارة",
        "order_number": "30517"
    },
    "reference": "order-30517"
}'
GET/v1/whatsapp/messages/{id}

Get a message

Reads a message's current status and timestamps. Statuses from Meta are followed for 48 hours after the send; `error` is set when the message failed. A message belongs to the key's application; any other id answers `404`.

Parameters

Response

200Delivered
{
    "id": "0f4c9c0e-6d2b-4b8a-9c3e-7a1d2e3f4a5b",
    "status": "delivered",
    "to": "+966550000000",
    "template": "order_received_v1",
    "lang": "ar",
    "reference": "order-1042",
    "price_halalas": 0,
    "error": null,
    "created_at": "2026-09-06T12:00:00+03:00",
    "sent_at": "2026-09-06T12:00:01+03:00",
    "delivered_at": "2026-09-06T12:00:04+03:00",
    "read_at": null,
    "failed_at": null
}
200Failed
{
    "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"
}
404Unknown id
{
    "error": "not_found"
}

Request example

curl https://tawked.com/v1/whatsapp/messages/0f4c9c0e-6d2b-4b8a-9c3e-7a1d2e3f4a5b \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx"
GET/v1/whatsapp/templates

List 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. Each row carries Meta's `id`, which the template operations take, and `managed`: true only when the calling key is a connected app's and that app created the template through this application. Cached for 5 minutes.

Parameters

Response

200The templates and their placeholders
{
    "data": [
        {
            "id": "7000",
            "name": "order_received_v1",
            "language": "ar",
            "category": "UTILITY",
            "status": "APPROVED",
            "placeholders": {
                "kind": "named",
                "header": [],
                "body": [
                    "name",
                    "order"
                ],
                "buttons": [
                    {
                        "index": 0,
                        "type": "url"
                    }
                ]
            },
            "managed": false
        }
    ]
}

Request example

curl https://tawked.com/v1/whatsapp/templates \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx"
GET/v1/whatsapp/templates/{template_id}

Get a template

Reads one template by its Meta `id`, in full: its status, quality and rejection reason, its components as Meta holds them, the sample values, the placeholders a send must fill, and `managed`. An id that is not on the number's WhatsApp Business Account answers `404`.

Parameters

Response

200The template
{
    "id": "7000",
    "name": "order_received_v1",
    "language": "ar",
    "category": "UTILITY",
    "status": "APPROVED",
    "quality": "GREEN",
    "rejected_reason": null,
    "components": [
        {
            "type": "HEADER",
            "format": "TEXT",
            "text": "طلبك {{1}}",
            "buttons": []
        },
        {
            "type": "BODY",
            "format": null,
            "text": "استلمنا طلبك رقم {{1}} بقيمة {{2}} ريال.",
            "buttons": []
        },
        {
            "type": "BUTTONS",
            "format": null,
            "text": null,
            "buttons": [
                {
                    "type": "URL",
                    "text": "تتبع الطلب",
                    "url": "https://flowers.example/orders/{{1}}",
                    "phone_number": null
                }
            ]
        }
    ],
    "examples": {
        "header:1": "1042",
        "body:1": "1042",
        "body:2": "250",
        "button:0": "1042"
    },
    "placeholders": {
        "kind": "positional",
        "header": [
            "1"
        ],
        "body": [
            "1",
            "2"
        ],
        "buttons": [
            {
                "index": 0,
                "type": "url"
            }
        ]
    },
    "managed": false
}
404Not on this number
{
    "error": "not_found"
}

Request example

curl https://tawked.com/v1/whatsapp/templates/7000 \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx"
POST/v1/whatsapp/templates

Create a template

For a connected app's key alone: an application's own key, full access included, answers `403 insufficient_scope`, and writes its templates in the console. Submits a text template to Meta's review under the rules of the console's editor, and answers Meta's `id`, the category as Meta filed it and the `status` (`PENDING` at first, usually). Text only: any field outside the ones below is ignored, so a template that opens with an image is created in the console. A refused field answers `400 invalid_request` with `fields` naming it and `errors` giving each field's reasons; what Meta refuses after that answers `422 template_refused` with Meta's own sentence.

Parameters

namestringRequired

Lowercase letters, digits and underscores, up to 512 characters. Meta fixes it at creation. A connected app's key may not take a name that starts `salla_`, `zid_` or `wf_`, or one of Tawked's own template names: `errors.name` is then `nameReserved`.

languagestringRequired

One of `ar`, `en`, `en_US`, `en_GB`, `ur`, `hi`, `bn`, `tl`, `id`, `fr`. Fixed at creation.

categorystringRequired

`UTILITY`. A connected app's key writes utility templates only: `MARKETING` is refused with `422 template_category_not_allowed`. An authentication template is created in the console.

headerstringOptional

A text header: one line, up to 60 characters, no emoji and no asterisk, at most one placeholder.

bodystringRequired

Up to 1024 characters. Placeholders are all named (`{{name}}`, lowercase) or all numbered (`{{1}}`, `{{2}}` in order), never at the very start or end of the text.

footerstringOptional

One line, up to 60 characters, no placeholders.

buttonsarrayOptional

Up to 10 objects, each with a `text` of up to 25 characters: `{ "type": "QUICK_REPLY", "text" }`, `{ "type": "URL", "text", "url" }` (https, optionally ending in `{{1}}`; two at most) or `{ "type": "PHONE_NUMBER", "text", "phone_number" }` (E.164; one at most). Quick replies sit together, before or after the other buttons.

examplesobjectOptional

One sample value per placeholder, keyed `header:<key>`, `body:<key>` and `button:<index>`. Required whenever the template has placeholders: Meta reviews with them.

Headers

Idempotency-KeystringOptional

A 1 to 128 printable-ASCII-character key you generate, scoped to your API key. Replaying it with the same request answers the original 2xx unchanged, with the response header `Idempotent-Replayed: true`, and runs nothing again; the same key with another request answers `409 idempotency_key_reused`, and a request still in flight `409 idempotency_in_progress`. A refused request is never stored, so a corrected retry under the same key runs. Keys expire after 24 hours.

Response

201Submitted to Meta
{
    "id": "7100",
    "name": "order_shipped_v1",
    "language": "ar",
    "category": "UTILITY",
    "status": "PENDING"
}
400Refused fields
{
    "error": "invalid_request",
    "fields": [
        "body",
        "examples.header:order"
    ],
    "errors": {
        "body": [
            "placeholderEdges"
        ],
        "examples.header:order": [
            "exampleRequired"
        ]
    }
}
422Meta refused it
{
    "error": "template_refused",
    "message": "Meta error 100: Template name already exists"
}

Request example

curl -X POST https://tawked.com/v1/whatsapp/templates \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_shipped_v1-ar" \
  -d '{
    "name": "order_shipped_v1",
    "language": "ar",
    "category": "UTILITY",
    "header": "طلبك {{order}}",
    "body": "طلبك {{order}} في الطريق، ويصلك خلال {{days}} أيام.",
    "footer": "متجر الورد",
    "buttons": [
        {
            "type": "URL",
            "text": "تتبع الشحنة",
            "url": "https://flowers.example/track/{{1}}"
        }
    ],
    "examples": {
        "header:order": "1042",
        "body:order": "1042",
        "body:days": "3",
        "button:0": "1042"
    }
}'
PATCH/v1/whatsapp/templates/{template_id}

Update a template

For a connected app's key alone: an application's own key, full access included, answers `403 insufficient_scope`, and writes its templates in the console. Replaces a text template's content on Meta. Send the whole new content: the body of a creation without `name` and `language`, which Meta fixed at creation and which are ignored here. Meta reviews the change again. Only an `APPROVED`, `REJECTED` or `PAUSED` template can be edited; anything else answers `409 template_not_editable` with a `status`: the template's own, `META_SAMPLE` for one of Meta's samples, or `MEDIA_HEADER` for a template that opens with an image or other media, which is edited in the console. An authentication template answers `422 template_category_not_allowed`. Answers the template as it stands after the edit.

Parameters

categorystringRequired

`UTILITY`. A connected app's key writes utility templates only: `MARKETING` is refused with `422 template_category_not_allowed`. An authentication template is created in the console.

headerstringOptional

A text header: one line, up to 60 characters, no emoji and no asterisk, at most one placeholder.

bodystringRequired

Up to 1024 characters. Placeholders are all named (`{{name}}`, lowercase) or all numbered (`{{1}}`, `{{2}}` in order), never at the very start or end of the text.

footerstringOptional

One line, up to 60 characters, no placeholders.

buttonsarrayOptional

Up to 10 objects, each with a `text` of up to 25 characters: `{ "type": "QUICK_REPLY", "text" }`, `{ "type": "URL", "text", "url" }` (https, optionally ending in `{{1}}`; two at most) or `{ "type": "PHONE_NUMBER", "text", "phone_number" }` (E.164; one at most). Quick replies sit together, before or after the other buttons.

examplesobjectOptional

One sample value per placeholder, keyed `header:<key>`, `body:<key>` and `button:<index>`. Required whenever the template has placeholders: Meta reviews with them.

Headers

Idempotency-KeystringOptional

A 1 to 128 printable-ASCII-character key you generate, scoped to your API key. Replaying it with the same request answers the original 2xx unchanged, with the response header `Idempotent-Replayed: true`, and runs nothing again; the same key with another request answers `409 idempotency_key_reused`, and a request still in flight `409 idempotency_in_progress`. A refused request is never stored, so a corrected retry under the same key runs. Keys expire after 24 hours.

Response

200The template after the edit
{
    "id": "7000",
    "name": "order_received_v1",
    "language": "ar",
    "category": "UTILITY",
    "status": "PENDING",
    "quality": null,
    "rejected_reason": null,
    "components": [
        {
            "type": "HEADER",
            "format": "TEXT",
            "text": "طلبك {{1}}",
            "buttons": []
        },
        {
            "type": "BODY",
            "format": null,
            "text": "استلمنا طلبك رقم {{1}} بقيمة {{2}} ريال، ونبلغك عند شحنه.",
            "buttons": []
        },
        {
            "type": "BUTTONS",
            "format": null,
            "text": null,
            "buttons": [
                {
                    "type": "URL",
                    "text": "تتبع الطلب",
                    "url": "https://flowers.example/orders/{{1}}",
                    "phone_number": null
                }
            ]
        }
    ],
    "examples": {
        "header:1": "1042",
        "body:1": "1042",
        "body:2": "250",
        "button:0": "1042"
    },
    "placeholders": {
        "kind": "positional",
        "header": [
            "1"
        ],
        "body": [
            "1",
            "2"
        ],
        "buttons": [
            {
                "index": 0,
                "type": "url"
            }
        ]
    },
    "managed": false
}
409Not editable
{
    "error": "template_not_editable",
    "status": "PENDING"
}
404Not on this number
{
    "error": "not_found"
}

Request example

curl -X PATCH https://tawked.com/v1/whatsapp/templates/7000 \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_received_v1-ar-2" \
  -d '{
    "category": "UTILITY",
    "header": "طلبك {{1}}",
    "body": "استلمنا طلبك رقم {{1}} بقيمة {{2}} ريال، ونبلغك عند شحنه.",
    "buttons": [
        {
            "type": "URL",
            "text": "تتبع الطلب",
            "url": "https://flowers.example/orders/{{1}}"
        }
    ],
    "examples": {
        "header:1": "1042",
        "body:1": "1042",
        "body:2": "250",
        "button:0": "1042"
    }
}'
DELETE/v1/whatsapp/templates/{template_id}

Delete a template

For a connected app's key alone: an application's own key, full access included, answers `403 insufficient_scope`, and writes its templates in the console. Deletes that one language of the template on Meta. Refused while something still sends it: `409 template_in_use`, with `used_by` saying what. Meta's own samples cannot be deleted (`409 template_not_editable` with `status` `META_SAMPLE`).

Parameters

Headers

Idempotency-KeystringOptional

A 1 to 128 printable-ASCII-character key you generate, scoped to your API key. Replaying it with the same request answers the original 2xx unchanged, with the response header `Idempotent-Replayed: true`, and runs nothing again; the same key with another request answers `409 idempotency_key_reused`, and a request still in flight `409 idempotency_in_progress`. A refused request is never stored, so a corrected retry under the same key runs. Keys expire after 24 hours.

Response

200Deleted
{
    "id": "7002",
    "name": "promo_v1",
    "language": "en",
    "deleted": true
}
409Still in use
{
    "error": "template_in_use",
    "used_by": [
        "campaign",
        "automation"
    ]
}
404Not on this number
{
    "error": "not_found"
}

Request example

curl -X DELETE https://tawked.com/v1/whatsapp/templates/7002 \
  -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx"

Message statuses

acceptedsentdeliveredreadfailed

failed can follow any of the first three, and it refunds the fee.

  • `accepted`: Tawked recorded the message and Meta took it. This is the `202`.
  • `sent`, `delivered`, `read`: Meta's statuses, applied in that order and never backwards. They reach Tawked through the status sync, which follows each message for 48 hours after the send.
  • `failed`: possible from any of the first three (an unreachable number, a paused template, a closed messaging window). `error` carries Meta's code and message.
  • The fee is for the send, never for delivery: `read` costs nothing more, and `failed` refunds it once. A message stops being tracked 48 hours after the send and keeps its last status.

WhatsApp webhooks

message.sentmessage.deliveredmessage.readmessage.failed
  • One event per status change, to the application's webhook URL, signed and retried exactly like `verification.*`: the same `tawked-timestamp`, `tawked-signature` and `tawked-event` headers, the same HMAC over `"{timestamp}.{raw body}"`, the same retry schedule, the same delivery log in the console.
  • The same envelope as `verification.*`: `event`, `data` and `sent_at`. The message's fields sit under `data`, with `occurred_at` the time of the status the event announces and `error` null unless the event is `message.failed` (then `{ "code", "message" }`). A test event from the console is always `verification.verified`, so no `message.*` event carries `test`.
  • Delivered by the status sync, which follows each message for 48 hours after the send. Only the status reached is announced: a message that jumps straight to `read` gets one `message.read`, not the rungs in between.
  • `to` is masked the way the console shows it. Keep your own copy of the destination next to the returned `id` or your `reference`.

Example payload

{
    "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"
}

WhatsApp errors

After the checks every endpoint shares: rate limit, authentication, account state and request shape, in this order.

StatuserrorMeaning
403live_key_requiredA sandbox (tk_test_) key. WhatsApp has no sandbox yet; use the application's live key.
403whatsapp_not_enabledWhatsApp is turned off for this application. Message support if you think this is a mistake.
409no_whatsapp_numberNo WhatsApp number is connected to this application yet. Connect one from the Number section of the application's Notifications tab in the dashboard (Getting started, step 1).
409number_disconnectedThe number is attached but not connected on Meta's side right now (its webhook is not claimed, or Meta reports a status other than CONNECTED). The console's Notifications page shows the number's health.
422invalid_destinationThe destination does not match any accepted Saudi mobile format. WhatsApp messages go to Saudi mobiles only.
422unknown_templateNo template with that name (and language, when given) on the number's WhatsApp Business Account. `GET /v1/whatsapp/templates` lists what exists.
422template_not_approvedThe template exists but Meta has not approved it; `message` carries Meta's status (PENDING, REJECTED, PAUSED, …).
422template_category_not_allowedAn AUTHENTICATION template sent with a partner key. Partner keys send utility and marketing templates; the application's own live key sends authentication ones. Also an authentication template in `PATCH /v1/whatsapp/templates/{template_id}` (its text is Meta's), and a connected app's key on any category but UTILITY, in a send or in a template it creates or edits.
422consent_opted_outA MARKETING template to a recipient who asked this application to stop its marketing messages (they wrote STOP to the number, or turned off its offers in WhatsApp). Nothing was sent, recorded or charged. Utility and authentication templates still reach them.
422unsupported_templateThe template needs something this endpoint does not carry yet: a video, document or location header (an image header is sent with `header.image.link`), a button or component that takes a non-text parameter (the copy-code button of a marketing coupon, flow, catalog, carousel, limited-time offer), or named and positional placeholders mixed in the text. `message` says which.
422invalid_paramsA placeholder value is missing, empty, unexpected, longer than 1024 characters or contains a newline, a tab or another control character (Meta refuses those), the template exists in several languages and `lang` was not given, an authentication template got `buttons` or a `code` that is not 4 to 15 letters or digits, or a template that opens with an image got no `header.image.link` (or one that is not https, or another template got one). `message` names the placeholder.
402insufficient_creditsThe account's prepaid balance is below the WhatsApp message fee. The body adds `balance_halalas` and `price_halalas`; nothing was sent or recorded.
502send_failedMeta refused the send or is unreachable. The body adds `provider_code` (Meta's error code) and `message`; the fee is refunded and the message is recorded as failed.
403template_not_managedA connected app's key on a template that app did not create for this application. Such a key edits and deletes only the templates it created, the ones `managed` is true on.
409template_not_editableThe template cannot be changed over the API as it stands; `status` says why. On an edit: the template's status when it is not `APPROVED`, `REJECTED` or `PAUSED`, or `MEDIA_HEADER` for a template that opens with an image or other media, which is edited in the console. On an edit or a delete: `META_SAMPLE` for one of Meta's own samples.
409template_in_useSomething still sends the template, so it is neither edited nor deleted. `used_by` lists what, from `campaign` (a campaign that has not finished), `automation` (an automation that is on) and `store_notice` (a store integration's notice that is on), in that order. The business stops it or gives it another template in the console first.
409idempotency_in_progressA request with this `Idempotency-Key` is still running. Wait for its answer, or retry in a moment.
409idempotency_key_reusedThis `Idempotency-Key` was already used for a different request. Use one key per change.
422template_refusedMeta refused the template after Tawked's own checks passed; `message` is Meta's own sentence (a name already in use, a text it will not review). Also a connected app's key that already holds as many templates as it may: `message` says so.
503templates_unavailableMeta did not answer the read of the number's template list, which the template endpoints and a send need before anything happens, or did not answer a template write. Nothing was sent, changed or charged; retry shortly.

Every error the API can return

StatuserrorMeaning
429too_many_requestsPer-key rate limit exceeded (default 120 requests/minute, fixed 60s window). Checked before authentication.
401unauthorizedMissing, unknown, revoked, or mode-mismatched key; or an unknown/suspended partner.
400service_requiredPartner key used with no `application` (or `service`) in the request body.
404service_not_foundPartner key, but `application`/`service` does not match any of that partner's provisioned applications.
403account_not_activeThe application is not approved yet, the account is banned, or a partner disabled this application. All four causes share this one response.
400invalid_jsonThe request body is not valid JSON.
400invalid_requeststart: `to` (and `phone`) both missing or blank. check: `id` or `code` missing.
422unsupported_channelUnknown channel name, or a known channel that is not live: WhatsApp until Tawked opens it for every account, or email: codes are not sent by email (Tawked Email sends a business's own mail, https://tawked.com/en/email).
422sandbox_unsupported_channelA sandbox (tk_test_) key on a channel that cannot carry the [TEST] stamp (WhatsApp's fixed Meta template has no room for it).
422invalid_destinationThe destination does not match any accepted Saudi mobile format.
422sandbox_unverified_destinationSandbox key, and the destination is not the phone the account owner verified at signup.
429sandbox_quota_exceededThe application's lifetime sandbox send cap (10) is reached.
429rate_limitedThe per-destination hourly cap is reached. Distinct from too_many_requests above: same HTTP status, different `error` string.
402insufficient_creditsThe account's prepaid balance is below the channel price.
502send_failedThe delivery provider call threw. The charge is refunded automatically.
404not_foundUnknown id, or an id that belongs to a different account or application than the calling key's.
500internal_errorAn unhandled server error. Quote the X-Request-Id header if you report it.
401key_expiredThe key's `expires_at` has passed. Checked right after the key is recognised, before the IP allowlist or scope.
403ip_not_allowedThe caller's IP does not match the key's `ip_allowlist` (exact IPs and/or CIDR ranges, IPv4 and IPv6).
403insufficient_scopeThe key was not granted this call: a custom key without this product, or with a level below the call, such as a key that checks where the call sends. The response header `WWW-Authenticate` names the scope the call needs. Judged right after the key is recognised, before `account_not_active` and the request's shape.
403application_pausedThe application is auto-paused by the pumping guard (a spike in distinct new destinations within an hour). The owner or an admin must resume it.
429ip_rate_limitedThe caller passed `client_ip`, and that address sent more starts in the last hour than the application's `max_per_ip_per_hour` allows.
429spend_cap_reachedToday's charges (Riyadh calendar day) plus this send would pass the application's `daily_spend_cap_halalas`.
409not_resendableThe verification is already in a terminal state (verified, failed, expired or canceled). The response includes the current `status`.
409not_cancelableThe verification is already in a terminal state. The response includes the current `status`.
429resend_limit_reachedThis verification already used its 3 resends.
403live_key_requiredA sandbox (tk_test_) key. WhatsApp has no sandbox yet; use the application's live key.
403whatsapp_not_enabledWhatsApp is turned off for this application. Message support if you think this is a mistake.
409no_whatsapp_numberNo WhatsApp number is connected to this application yet. Connect one from the Number section of the application's Notifications tab in the dashboard (Getting started, step 1).
409number_disconnectedThe number is attached but not connected on Meta's side right now (its webhook is not claimed, or Meta reports a status other than CONNECTED). The console's Notifications page shows the number's health.
422unknown_templateNo template with that name (and language, when given) on the number's WhatsApp Business Account. `GET /v1/whatsapp/templates` lists what exists.
422template_not_approvedThe template exists but Meta has not approved it; `message` carries Meta's status (PENDING, REJECTED, PAUSED, …).
422template_category_not_allowedAn AUTHENTICATION template sent with a partner key. Partner keys send utility and marketing templates; the application's own live key sends authentication ones. Also an authentication template in `PATCH /v1/whatsapp/templates/{template_id}` (its text is Meta's), and a connected app's key on any category but UTILITY, in a send or in a template it creates or edits.
422consent_opted_outA MARKETING template to a recipient who asked this application to stop its marketing messages (they wrote STOP to the number, or turned off its offers in WhatsApp). Nothing was sent, recorded or charged. Utility and authentication templates still reach them.
422unsupported_templateThe template needs something this endpoint does not carry yet: a video, document or location header (an image header is sent with `header.image.link`), a button or component that takes a non-text parameter (the copy-code button of a marketing coupon, flow, catalog, carousel, limited-time offer), or named and positional placeholders mixed in the text. `message` says which.
422invalid_paramsA placeholder value is missing, empty, unexpected, longer than 1024 characters or contains a newline, a tab or another control character (Meta refuses those), the template exists in several languages and `lang` was not given, an authentication template got `buttons` or a `code` that is not 4 to 15 letters or digits, or a template that opens with an image got no `header.image.link` (or one that is not https, or another template got one). `message` names the placeholder.
403template_not_managedA connected app's key on a template that app did not create for this application. Such a key edits and deletes only the templates it created, the ones `managed` is true on.
409template_not_editableThe template cannot be changed over the API as it stands; `status` says why. On an edit: the template's status when it is not `APPROVED`, `REJECTED` or `PAUSED`, or `MEDIA_HEADER` for a template that opens with an image or other media, which is edited in the console. On an edit or a delete: `META_SAMPLE` for one of Meta's own samples.
409template_in_useSomething still sends the template, so it is neither edited nor deleted. `used_by` lists what, from `campaign` (a campaign that has not finished), `automation` (an automation that is on) and `store_notice` (a store integration's notice that is on), in that order. The business stops it or gives it another template in the console first.
409idempotency_in_progressA request with this `Idempotency-Key` is still running. Wait for its answer, or retry in a moment.
409idempotency_key_reusedThis `Idempotency-Key` was already used for a different request. Use one key per change.
422template_refusedMeta refused the template after Tawked's own checks passed; `message` is Meta's own sentence (a name already in use, a text it will not review). Also a connected app's key that already holds as many templates as it may: `message` says so.
503templates_unavailableMeta did not answer the read of the number's template list, which the template endpoints and a send need before anything happens, or did not answer a template write. Nothing was sent, changed or charged; retry shortly.

Rate limits

120 requests per minute per key by default, in a fixed 60-second window. It fails open on a cache error: defense in depth on top of your balance and the per-destination cap, never the primary control.

Request id

Every response carries an X-Request-Id header. Quote it if you write to support about a specific call.

Client webhooks

Configure a URL and a signing secret per application, and get a signed request for every outcome, with no polling.

verification.verifiedverification.failedverification.expired
  • Configured per application on its Webhooks page: a URL and a secret, revealed on demand and rotatable. An application a partner provisioned has no webhook of its own: its verification outcomes go to the partner's event log and, when the partner subscribed to them, to the partner webhook, with the partner's `external_id`.
  • `verification.verified` and `verification.failed` fire from `check`; `verification.expired` fires from the background expiry sweep, and from any `check` or `GET` that discovers the expiry itself.
  • Every delivery carries `tawked-timestamp`, `tawked-signature` (hex HMAC-SHA256 of `"{timestamp}.{raw body}"` with your secret) and `tawked-event`. Verify the signature against the raw request body before trusting any payload.
  • Delivery retries automatically with backoff on anything but a 2xx response, with the same job and schedule as partner webhooks; the exact retry count is below. The delivery log in the console shows every attempt.
  • The URL must be https on a public host, and your endpoint must answer itself: a redirect is not followed and counts as a failed attempt.
  • A test event sent from the console's Webhooks page is one sample `verification.verified` whose envelope also carries `"test": true`, with made-up values under `data`: an id of zeros, the reference `test-event`, the mode `test`. No real event carries `test`, so skip your own side effects when it is there.
  • An application with WhatsApp also receives `message.sent`, `message.delivered`, `message.read` and `message.failed` at the same URL, in the same envelope (see WhatsApp messages).

Example payload

{
    "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"
}

Verify the signature in Node.js

const [timestamp, signature] = [
  req.headers["tawked-timestamp"],
  req.headers["tawked-signature"],
];
const expected = crypto
  .createHmac("sha256", webhookSecret)
  .update(`${timestamp}.${rawBody}`)
  .digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) throw new Error("bad signature");

Retried on anything but a 2xx response, up to 5 attempts, backing off by minutes: 1, 5, 30, 120, 360.

Partner API

Reseller platforms, like Salla or Zid apps, use a single tk_partner_ key to provision many stores over /v1/partner/applications, with webhooks for review and balance events.

Partner guide

This reference is built for machines too, as raw Markdown: /llms.txt · /llms-full.txt

Chat on WhatsApp