Skip to main content
An application is what a merchant sends under: a reviewed brand (two names and a website), the verify settings, and the state that says whether it may send. You address it by your own external_id; Tawked’s application_id comes back in every response, but you never need to store it.

Idempotent writes

Every write on the partner API takes an Idempotency-Key header: 1 to 128 printable ASCII characters you generate, one per business event, 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: no second SMS to a merchant, no second application. The same key with a different request answers 409 idempotency_key_reused, and a request still in flight 409 idempotency_in_progress. A refused request is never stored, so fix it and retry under the same key. Keys expire after 24 hours.
Provisioning is idempotent on external_id even without the header; the header adds the guarantee that a retried call answers exactly what the first one did.

Provision

POST /v1/partner/applications creates the application, screens its names, and either queues it for review or activates it at once, according to your approval mode.
The answer is 201 the first time and 200 when the external_id already exists. Both carry the application as it stands:
Provisioning is idempotent on external_id at the database level, so an install that runs twice, or two installs racing, end with one application. A repeat answers 200 with the current state and applies nothing from the new body, verify included. To change settings later, use the verify-settings endpoint.
A refused field answers 400 invalid_request with fields naming it in your own spelling, for example ["external_id", "website", "verify.code_length"].

Provision in bulk

POST /v1/partner/applications/batch takes up to 100 provisioning bodies in one call, for a migration or a nightly sync:
Each item is exactly the body of a single provision. Items run in order, one at a time, each in its own transaction and with its own events. The answer is always 200 with the counts and one result per item, in the same order:
  • outcome is created (screened, then queued or activated like a single provision), existing (the external_id was already yours: the current state, nothing applied from the item) or refused (fields names the refused fields as a single provision would, application is null, and the rest of the batch still runs).
  • The same external_id twice in one batch is created then existing.
  • applications must be a list of 1 to 100 objects; anything else answers 400 invalid_request with fields: ["applications"]. An item that is not an object is refused with fields: ["item"].
  • Send an Idempotency-Key: a replay answers the same envelope and provisions nothing again.
  • Every created application fires its own events and enters your review queue as usual, so a batch of a hundred under a manual approval mode is a hundred reviews.

What happens after provisioning

Both names are screened against protected brands, bait words and structural rules (a URL, a phone number or an email inside a name). The verdict comes back as screening.result, clean or flagged, with the rules that fired: protected_brand: followed by the category and the term, bait_word: followed by the word, or structural:url, structural:phone, structural:email, structural:too_short, structural:too_long, structural:bidi_or_invisible, structural:control_chars. What happens next depends on the approval mode Tawked set on your account: There is no approve or reject endpoint. Under auto-clean nothing decides a clean name at all; otherwise the decision is made in a console, yours when your mode allows it and Tawked’s when it does not. Your backend learns the outcome from the service.approved and service.rejected webhooks, or by reading the application. Two more switches live beside status and never change it:

List and read

GET /v1/partner/applications lists your applications newest first, with status, disabled (true or false), limit (1 to 100) and offset as query parameters; total counts the matches before paging. GET /v1/partner/applications/{external_id} reads one. Both print the same row: the fields above plus name_ar, name_en, disabled and created_at.
An external_id that is not yours answers 404 not_found, whether or not it exists under another partner.

What a row carries

Metadata

metadata is yours: up to 20 string values of at most 200 characters, under keys of 1 to 40 lowercase letters, digits and underscores. Send it on provision or on a brand update; it is replaced whole when present, unchanged when omitted, and cleared by null or {}. Tawked never reads it and it is not a brand change, so it never sends an application back to review. A refused key comes back in fields as metadata. followed by the key.
The list filters by containment: GET /v1/partner/applications?metadata[plan]=pro&metadata[region]=riyadh returns the applications whose metadata holds every pair. POST /v1/partner/applications/{external_id}/logo takes a multipart upload in the field logo: a PNG or a JPEG of at most 256 KB, recognised by its bytes and never by its name. It is stored where the console stores it and comes back as logo_url on every row. A new logo is a brand change, so an active application returns to review until it is approved again, exactly like a renamed one. DELETE .../logo drops it without a review.

History

GET /v1/partner/applications/{external_id}/history is the review history the console shows, newest first: provisioned, submitted, approve, reject, suspend, resume, brand_changed, settings_changed, renamed, each with actor (partner, tawked, system or client, never a person), reason and the screening verdict at that moment.

Screening preview

POST /v1/partner/screening with name_ar and name_en answers the verdict the names would get and what would happen next under your approval mode, without creating anything: review.needed says whether a provision would wait for a decision, review.decided_by whether that decision is yours (partner) or Tawked’s. Warn the merchant at signup instead of after.

Update the brand

PATCH /v1/partner/applications/{external_id} replaces the brand in full: name_ar, name_en and website are required on every call, industry, merchant and verify are optional. The names are screened again. A change to a name or the website returns an active application to review until it is approved again, and codes refuse meanwhile (WhatsApp sends are not held back by review); a call that changes none of the three keeps the status, so a nightly sync that sends the same brand is harmless.
The answer is the same body as provisioning, with the new status and screening.

Verify settings

PATCH /v1/partner/applications/{external_id}/verify-settings changes any subset of the settings. Send the fields at the top level or nested under verify; omitted fields stay as they are. A value out of range refuses the whole request with the offending names in fields, nothing is clamped or half-applied, and an empty or unrecognised patch is refused with fields: ["verify"]. Nothing here re-screens the names or changes the status.
autofill_domain is read-only: it is derived from the website (its registrable domain) and comes back in every response. Destinations and messages explains what the lines do. The answer is every setting in force after the change:

Suspend, resume and unpause

Suspend is your own off switch, for a merchant that left your platform or stopped paying you: every send answers 403 account_not_active until you resume. The review status is untouched, so an application approved before comes back sending at once, and suspending fires service.suspended. Both answer { "external_id": "store_88", "disabled": true } or false, and both are safe to repeat. Unpause is different: it lifts an automatic pause by the protection guard after a spike in distinct new destinations within an hour, announced by service.paused and visible as paused: true. Codes answer 403 application_paused meanwhile. Look at what the merchant was doing before you unpause; the guard exists to stop SMS pumping within seconds.

Archive and restore

POST /v1/partner/applications/{external_id}/archive is your delete, and it is reversible. An archived application leaves the list, the counts and the review queues, every send answers 403 account_not_active, and every other write answers 409 application_archived; the history, the usage and the ledger stay, and GET .../{external_id} still reads it with archived: true and archived_at. The list shows archived applications only when asked: GET /v1/partner/applications?archived=true. Two things must be gone first: a WhatsApp number in any state (409 number_connected, the inbox belongs to the team that runs it) and an owner (409 owner_assigned, revoke first). A pending invitation is withdrawn by the archive itself. Archiving fires service.archived. POST .../restore brings it back exactly as it was: the status, your switch, the pause and the settings are untouched, and service.restored fires. A merchant that installs again restores it too: provisioning an archived external_id answers 200 with the application restored, since provisioning is idempotent on the id. Both calls answer the same when there is nothing to do.
There is no delete beyond this: an archived application keeps its external_id.

Usage

GET /v1/partner/usage reports, per application and over a window, the codes that went out (sent, whatever their outcome), the ones confirmed (verified) and the charges net of refunds (spend_halalas), plus your current balance. from and to are ISO 8601; to defaults to now and from to 30 days before it. Applications with no activity in the window are left out, rows are sorted by sent, and there is no paging.
The same report is a CSV in the partner console under Usage.