> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tawked.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Applications

> Provision an application per merchant, keep its brand and settings current, switch it off and on, and read usage.

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.

```bash theme={"dark"}
curl -X POST https://tawked.com/v1/partner/applications/store_88/owner \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: store_88-owner-invite-1" \
  -H "Content-Type: application/json" \
  -d '{ "name": "أحمد", "phone": "0551234567" }'
```

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`](/api-reference/partner/provision-an-application) creates the application, screens its names, and either queues it for review or activates it at once, according to your approval mode.

```bash theme={"dark"}
curl -X POST https://tawked.com/v1/partner/applications \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "store_88",
    "name_ar": "متجر الورد",
    "name_en": "Flower Shop",
    "website": "https://flowers.example",
    "industry": "ecommerce",
    "merchant": { "registration_number": "1010123456", "contact_email": "owner@flowers.example" },
    "verify": { "code_length": 6, "ttl_seconds": 300 }
  }'
```

| Field                | Required | Notes                                                                                                                             |
| -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `external_id`        | yes      | Your own id for the merchant or store, 1 to 128 characters. Every later call uses it, and it is the `application` value on sends. |
| `name_ar`, `name_en` | yes      | The brand names end users see in their messages, 1 to 80 characters each. Screened against protected brands and bait words.       |
| `website`            | yes      | An `http` or `https` URL. The autofill domain is derived from it and is read-only. `website_url` is accepted as an alias.         |
| `industry`           | no       | Free text up to 64 characters, `other` when omitted.                                                                              |
| `merchant`           | no       | Your own record of the merchant, stored as given, never validated for shape.                                                      |
| `verify`             | no       | Initial verify settings, any subset of the [fields below](#verify-settings). Defaults for the rest.                               |

The answer is `201` the first time and `200` when the `external_id` already exists. Both carry the application as it stands:

```json theme={"dark"}
{
  "service_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
  "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
  "external_id": "store_88",
  "status": "review",
  "screening": { "result": "clean", "rules": [] },
  "verify": {
    "code_length": 6,
    "ttl_seconds": 300,
    "max_attempts": 3,
    "max_per_hour": 5,
    "autofill_domain": "flowers.example",
    "autofill_domain_enabled": true,
    "android_app_hash": null,
    "daily_spend_cap_halalas": null,
    "max_per_ip_per_hour": 20,
    "max_new_destinations_per_hour": 300
  },
  "paused": false
}
```

<Tip>
  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.
</Tip>

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`](/api-reference/partner/provision-in-bulk) takes up to 100 provisioning bodies in one call, for a migration or a nightly sync:

```bash theme={"dark"}
curl -X POST https://tawked.com/v1/partner/applications/batch \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sync-2026-09-27" \
  -d '{
    "applications": [
      { "external_id": "store_88", "name_ar": "متجر الورد", "name_en": "Flower Shop", "website": "https://flowers.example" },
      { "external_id": "store_89", "name_ar": "مخبز السنابل", "name_en": "Sanabel Bakery", "website": "https://sanabel.example" }
    ]
  }'
```

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:

```json theme={"dark"}
{
  "created": 1,
  "existing": 1,
  "refused": 0,
  "results": [
    { "index": 0, "external_id": "store_88", "outcome": "existing", "application": { "service_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f", "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f", "external_id": "store_88", "status": "active", "screening": { "result": "clean", "rules": [] }, "verify": { "code_length": 6, "ttl_seconds": 300, "max_attempts": 3, "max_per_hour": 5, "autofill_domain": "flowers.example", "autofill_domain_enabled": true, "android_app_hash": null, "daily_spend_cap_halalas": null, "max_per_ip_per_hour": 20, "max_new_destinations_per_hour": 300 }, "paused": false } },
    { "index": 1, "external_id": "store_89", "outcome": "created", "application": { "service_id": "9d1c2b3a-4e5f-4a6b-8c7d-0e1f2a3b4c5d", "application_id": "9d1c2b3a-4e5f-4a6b-8c7d-0e1f2a3b4c5d", "external_id": "store_89", "status": "review", "screening": { "result": "clean", "rules": [] }, "verify": { "code_length": 6, "ttl_seconds": 300, "max_attempts": 3, "max_per_hour": 5, "autofill_domain": "sanabel.example", "autofill_domain_enabled": true, "android_app_hash": null, "daily_spend_cap_halalas": null, "max_per_ip_per_hour": 20, "max_new_destinations_per_hour": 300 }, "paused": false } }
  ]
}
```

* `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:

| Approval mode             | Clean names                             | Flagged names |
| ------------------------- | --------------------------------------- | ------------- |
| Tawked reviews everything | Tawked                                  | Tawked        |
| You approve clean names   | You, in the console's **Reviews** queue | Tawked        |
| You approve everything    | You                                     | You           |
| Auto-clean                | Active at once, no human at all         | Tawked        |

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](/partners/webhooks), or by reading the application.

| `status`    | Meaning                                                                        | Codes                    | WhatsApp                          |
| ----------- | ------------------------------------------------------------------------------ | ------------------------ | --------------------------------- |
| `review`    | Waiting for a decision.                                                        | `403 account_not_active` | Allowed: review gates codes only. |
| `active`    | Approved.                                                                      | Allowed                  | Allowed                           |
| `rejected`  | Refused, with a reason in the webhook. Fix the brand with a PATCH to resubmit. | `403 account_not_active` | `403 account_not_active`          |
| `suspended` | Suspended by Tawked. Contact support.                                          | `403 account_not_active` | `403 account_not_active`          |

Two more switches live beside `status` and never change it:

| Field            | Set by                                                                                                                                   | Codes                    | WhatsApp                          |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | --------------------------------- |
| `disabled: true` | You, with [suspend](/api-reference/partner/suspend-an-application); cleared with [resume](/api-reference/partner/resume-an-application). | `403 account_not_active` | `403 account_not_active`          |
| `paused: true`   | The protection guard, after a spike in new destinations; cleared with [unpause](/api-reference/partner/unpause-an-application).          | `403 application_paused` | Allowed: the guard watches codes. |

## List and read

[`GET /v1/partner/applications`](/api-reference/partner/list-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}`](/api-reference/partner/get-an-application) reads one. Both print the same row: the fields above plus `name_ar`, `name_en`, `disabled` and `created_at`.

```bash theme={"dark"}
curl "https://tawked.com/v1/partner/applications?status=review&limit=50" \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx"
```

An `external_id` that is not yours answers `404 not_found`, whether or not it exists under another partner.

### What a row carries

| Field                                                               | Meaning                                                                                                                                                                                                                                                           |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `service_id`, `application_id`                                      | Tawked's id, twice (the first is the legacy name).                                                                                                                                                                                                                |
| `external_id`                                                       | Yours.                                                                                                                                                                                                                                                            |
| `status`, `status_message`                                          | `review`, `active`, `rejected` or `suspended`, and the reason Tawked or you gave on a rejection or a suspension.                                                                                                                                                  |
| `screening`                                                         | The verdict on the names: `result` and the rules that fired.                                                                                                                                                                                                      |
| `verify`                                                            | The verify settings in force.                                                                                                                                                                                                                                     |
| `paused`, `paused_at`, `pause_reason`                               | The protection guard's pause, when it landed and why (`destination_spike`).                                                                                                                                                                                       |
| `disabled`, `disabled_at`                                           | Your own switch and when you flipped it.                                                                                                                                                                                                                          |
| `archived`, `archived_at`                                           | Whether you [archived](#archive-and-restore) it, and when.                                                                                                                                                                                                        |
| `approved_at`                                                       | When the application was last approved, `null` while it waits or after a rejection.                                                                                                                                                                               |
| `name_ar`, `name_en`, `website`, `industry`, `merchant`, `logo_url` | The brand as you sent it, plus the logo you uploaded.                                                                                                                                                                                                             |
| `metadata`                                                          | Your own tags, or `null`.                                                                                                                                                                                                                                         |
| `owner`                                                             | `status` (`none`, `invited`, `accepted`), the person's `name` and `phone`, `invited_at`, `expires_at`, `accepted_at`. The same states as the [owner endpoint](/partners/owners), without the email.                                                               |
| `whatsapp`                                                          | `enabled`, and the `number` when one is attached: `phone`, `verified_name`, `connected`, `health` (`pending`, `healthy`, `degraded`, `blocked`, `disconnected`) and `stage` (`setting_up`, `action_needed`, `add_payment`, `meta_reviewing`, `limited`, `ready`). |
| `created_at`                                                        | When you provisioned it.                                                                                                                                                                                                                                          |

## 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.

```bash theme={"dark"}
curl -X PATCH https://tawked.com/v1/partner/applications/store_88 \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "name_ar": "متجر الورد", "name_en": "Flower Shop", "website": "https://flowers.example", "metadata": { "plan": "pro", "region": "riyadh" } }'
```

The list filters by containment: `GET /v1/partner/applications?metadata[plan]=pro&metadata[region]=riyadh` returns the applications whose metadata holds every pair.

## Logo

[`POST /v1/partner/applications/{external_id}/logo`](/api-reference/partner/upload-the-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`](/api-reference/partner/remove-the-logo) drops it without a review.

```bash theme={"dark"}
curl -X POST https://tawked.com/v1/partner/applications/store_88/logo \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx" \
  -F "logo=@logo.png"
```

## History

[`GET /v1/partner/applications/{external_id}/history`](/api-reference/partner/get-the-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.

```json theme={"dark"}
{
  "external_id": "store_88",
  "events": [
    { "action": "approve", "actor": "partner", "reason": null, "screening": { "result": "clean", "rules": [] }, "created_at": "2026-09-02T09:00:00.000Z" },
    { "action": "provisioned", "actor": "system", "reason": null, "screening": { "result": "clean", "rules": [] }, "created_at": "2026-09-01T12:00:00.000Z" }
  ]
}
```

## Screening preview

[`POST /v1/partner/screening`](/api-reference/partner/preview-the-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.

```json theme={"dark"}
{ "result": "flagged", "rules": ["protected_brand:banks:alrajhi"], "review": { "needed": true, "decided_by": "tawked" } }
```

## Update the brand

[`PATCH /v1/partner/applications/{external_id}`](/api-reference/partner/update-the-brand) 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.

```bash theme={"dark"}
curl -X PATCH https://tawked.com/v1/partner/applications/store_88 \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "name_ar": "متجر الورد الجديد", "name_en": "New Flower Shop", "website": "https://flowers.example" }'
```

The answer is the same body as provisioning, with the new `status` and `screening`.

## Verify settings

[`PATCH /v1/partner/applications/{external_id}/verify-settings`](/api-reference/partner/update-the-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.

```bash theme={"dark"}
curl -X PATCH https://tawked.com/v1/partner/applications/store_88/verify-settings \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "code_length": 8, "ttl_seconds": 600, "android_app_hash": "FA+9qCX9VSu" }'
```

| Field                           | Default      | Bounds                             | What it does                                                                                                     |
| ------------------------------- | ------------ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `code_length`                   | 6            | 4 to 8                             | Digits in the code.                                                                                              |
| `ttl_seconds`                   | 300          | one of 60, 120, 180, 300, 600, 900 | How long a code lives, in seconds.                                                                               |
| `max_attempts`                  | 3            | 1 to 5                             | Wrong codes allowed per verification.                                                                            |
| `max_per_hour`                  | 5            | 1 to 10                            | Sends to one destination per hour.                                                                               |
| `autofill_domain_enabled`       | `true`       | boolean                            | Whether the `@domain #code` line goes out at the end of the SMS.                                                 |
| `android_app_hash`              | `null`       | 11 characters, or `null` to clear  | The hash for Google's SMS Retriever on Android.                                                                  |
| `daily_spend_cap_halalas`       | `null` (off) | 100 to 100000000, or `null`        | A cap on one day's charges (Riyadh calendar day). Over it, sends answer `429 spend_cap_reached`.                 |
| `max_per_ip_per_hour`           | 20           | 1 to 1000                          | Starts per end-user IP per hour, when you pass `client_ip`. Over it, that address answers `429 ip_rate_limited`. |
| `max_new_destinations_per_hour` | 300          | 10 to 100000                       | Distinct new destinations per hour. Crossing it pauses the application.                                          |

`autofill_domain` is read-only: it is derived from the website (its registrable domain) and comes back in every response. [Destinations and messages](/destinations#autofill-ready-sms) explains what the lines do.

The answer is every setting in force after the change:

```json theme={"dark"}
{
  "external_id": "store_88",
  "verify": {
    "code_length": 8,
    "ttl_seconds": 600,
    "max_attempts": 3,
    "max_per_hour": 5,
    "autofill_domain": "flowers.example",
    "autofill_domain_enabled": true,
    "android_app_hash": "FA+9qCX9VSu",
    "daily_spend_cap_halalas": null,
    "max_per_ip_per_hour": 20,
    "max_new_destinations_per_hour": 300
  }
}
```

## Suspend, resume and unpause

[Suspend](/api-reference/partner/suspend-an-application) 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](/api-reference/partner/resume-an-application). 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](/api-reference/partner/unpause-an-application) 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`](/api-reference/partner/archive-an-application) 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`](/api-reference/partner/restore-an-application) 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.

```bash theme={"dark"}
curl -X POST https://tawked.com/v1/partner/applications/store_88/archive \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: store_88-archive-1"
```

There is no delete beyond this: an archived application keeps its `external_id`.

## Usage

[`GET /v1/partner/usage`](/api-reference/partner/get-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.

```bash theme={"dark"}
curl "https://tawked.com/v1/partner/usage?from=2026-08-01&to=2026-08-31" \
  -H "Authorization: Bearer tk_partner_xxxxxxxxxxxxxxxxxxxx"
```

```json theme={"dark"}
{
  "from": "2026-08-01T00:00:00.000Z",
  "to": "2026-08-31T00:00:00.000Z",
  "balance_halalas": 123456,
  "applications": [
    { "external_id": "store_88", "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f", "name_ar": "متجر الورد", "sent": 41, "verified": 33, "spend_halalas": 369 }
  ]
}
```

The same report is a CSV in the partner console under **Usage**.
