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

# Account API

> Everything a person does in the Tawked console, for an AI agent or a script: account tokens, permissions, the agent's loop, the person in the loop, conventions, errors, limits, and two worked examples.

The Account API lets an agent or a script do what a person does in the Tawked console: create and check WhatsApp templates, read messages, add members and support team members, add an e-mail sending domain, run campaigns and automations, read the wallet, and the rest. Every console write has its operation, and every operation runs the console's own rules.

It sits beside the sending API, on the same base URL, with a different credential:

| | What it is for | Credential |
| - | - | - |
| **Sending API** | Your servers send codes, WhatsApp messages and e-mails: `/v1/verify/*`, `/v1/whatsapp/messages`, `/v1/email/messages`, the Partner API | An application key (`tk_live_`, `tk_test_`, `tk_partner_`), see [Authentication](/authentication) |
| **Account API** | An agent or a script sets up and runs the account: `/v1/me`, `/v1/operations`, `/v1/account/…`, `/v1/applications/{application}/…` | An account token (`tk_acct_`) |
| **[MCP server](/ai/mcp-server)** | An assistant (Claude, ChatGPT, Cursor, Codex, Claude Code) calls the Account API through its tools | The assistant's sign-in (which makes an account token), or an account token |

An application key never opens the Account API, and an account token never opens the sending API. Keep sending with application keys from your servers; use an account token to set things up and to check on them.

Base URL: `https://tawked.com/v1`. JSON in and out.

<Tip>
  To connect an agent now, follow the [quickstart](/account-api/quickstart): the steps for Claude Code, Claude, ChatGPT, Cursor, Codex and a script, and the prompt to give the agent.
</Tip>

## Account tokens

Make a token in the console under **Settings → Account API**, with **Create token**. Anyone with console access makes tokens for themselves. The token is shown once: put it in your agent's environment as `TAWKED_TOKEN` (or its secret storage), never into source code or a chat. The reveal's instructions for your agent hold no secret: they tell it to send `Authorization: Bearer $TAWKED_TOKEN`. An assistant that signs in to the [MCP server](/ai/mcp-server) gets a token of its own, made by its consent page: you never copy that one, and it has its own page in the same list, marked **Assistant**.

```bash theme={"dark"}
curl https://tawked.com/v1/me \
  -H "Authorization: Bearer tk_acct_xxxxxxxxxxxxxxxxxxxx"
```

* **It acts as you, never more.** A call made with your token is your call: your role, the applications you reach, and every rule the console applies to you. A member who may not delete a template in the console may not delete it with a token either.
* **Permissions narrow it.** Choose **Everything you can do** (`*`, every area now and later), **Read only** (every area's `read`), or **Choose per area**: read, or read and write, area by area. [Permissions](/account-api/permissions) lists the areas.
* **Applications narrow it.** **All your applications** (the ones you reach now and the ones added later), or only those you choose. An application outside the token's choice answers `404 not_found`, as if it did not exist, whether it is in the path, the body or the query. A token narrowed to some applications reads the account's lists (alerts, e-mail suppressions, the wallet's statement) as a member limited to those applications would, and may not change who works in the account: a members write answers `403 token_narrowed` with a `widen_applications` [`human_action`](/account-api/person-in-the-loop).
* **Expiry.** Never, or after 30 days, 90 days or 1 year. An expired token answers `401 token_expired`, and a revoked one `401 token_revoked`, each with a `new_token` [`human_action`](/account-api/person-in-the-loop) that sends the person to make a new one.
* **Approvals.** **Ask me first** (the default) holds every consequential call until you approve it in the console; **Act without asking** is for a script you trust. See [Approvals](/account-api/person-in-the-loop#approvals).
* **Addresses.** Optionally, only from the IP addresses or ranges you list; any other answers `403 ip_not_allowed`.
* **Revoke.** On the same page, **Revoke token** refuses every request that carries it at once. You see and revoke your own tokens; the account owner sees and revokes every token of the account. `DELETE /v1/account/tokens/{token}` does the same from the API.
* **Recorded.** Every call made with a token is on record for 90 days (`account.tokens.activity`, and the token's page), and every change it made is in the account's record, with the token (or the assistant) that made it. A new token e-mails its person, and the owner when a member made it.

[Security](/account-api/security) has all of it. A token is refused at the door when the person could not open the console either: `403 account_suspended` (the owner sees why in the console), `403 onboarding_incomplete` (finish setting up the account first: the answer carries the link, see below), `403 setup_pending` (the account owner has not finished setup), `403 no_console_access` (a person who answers in Tawked Chat only).

## The agent's loop

An agent does not need to know the operations in advance. The API describes itself:

<Steps>
  <Step title="Who am I">
    `GET /v1/me` answers the person, the account, the token's permissions and expiry, the person's capabilities, and the applications the token reaches with their products. Call it first: most paths need an application's `id` from it.
  </Step>

  <Step title="Find the operation">
    `GET /v1/operations?q=whatsapp+template+create` searches every operation by words (`q`) and optionally by `area`, best matches first. Each result has its `id`, `method`, `path`, `summary`, `permission`, whether it is `consequential`, and `allowed`: whether this token may call it. When `allowed` is false, `needs` names the permission its person would add; calling it answers `403 insufficient_permission` with the link to add it.
  </Step>

  <Step title="Read its input">
    `GET /v1/operations/whatsapp.templates.create` answers the operation in full: its path parameters (`parameters`: for each, what it is and `list_with`, the operation that lists its values, such as `whatsapp.templates.list` for `{template}`), the JSON Schema of its body (a write) or its query (a read), `allowed`, and `next`, how to call it (the method and path, the path parameters, an `Idempotency-Key` on a write, and for a consequential operation on a token that asks first, the `428` loop). When the token may not call it, the answer adds `needs` and a `grant_permission` `human_action` to relay. An id that does not exist answers `404 not_found` with `did_you_mean`, the nearest operations.
  </Step>

  <Step title="Call it">
    The method and path from step 2, with the body from step 3. When there is a next step, the answer's `next` says it in a sentence that names the operation to call and, when the step is to wait, how often to read it again (for example `whatsapp.templates.show` every 30 to 60 seconds while Meta reviews a template).
  </Step>

  <Step title="Hand the person what is theirs">
    When an answer carries `human_action`, a person must act: tell them its `say`, give them its `url`, and once they say it is done call its `then`. A `428 approval_required` is one of these: see [The person in the loop](/account-api/person-in-the-loop).
  </Step>
</Steps>

An operation's `id` (`<area>.<resource>.<verb>`, such as `email.senders.check`) is stable: write it into prompts and scripts. Every operation is also in the [reference](#the-reference), generated from the same registry.

## Conventions

* **Answers.** Success is `{"data": …}`; an operation that makes something answers `201`, the reference says which. A write with nothing to report answers `{"data": {"ok": true}}`. A refusal is `{"error": "…", "message": "…"}` ([Errors](#errors)). A `2xx` always means the operation ran; a call held for its person's yes answers `428 approval_required` ([Approvals](/account-api/person-in-the-loop#approvals)).
* **Where `next` and `human_action` sit.** On a success they are inside `data` (`data.next`, `data.human_action`); on a refusal they are beside `error`, at the top of the answer (`next`, `human_action`). Every refusal carries `next` ([Errors](#errors)); a success carries it when something is left to do or wait for.
* **What next.** An answer with a next step carries `next`: a sentence naming the operation to call and, for a wait, how often to read again (`whatsapp.messages.show` every 5 to 10 seconds while a message is on its way, `whatsapp.templates.show` every 30 to 60 seconds while Meta reviews, a decision by Tawked's team every few hours). It follows the state of what it answers: a template that is ready to create, a message that failed (with what Meta's code means), a campaign by its status and pause reason, an automation and its runs, the WhatsApp number, Tawked Email by its stage, an open proforma, a low balance, Radeef, the auto-attendant, an import. It is never a bare id, and it is left out once nothing is left to do. List rows carry none (read the row's show), except open proformas.
* **Lists.** `{"data": [...], "next_cursor": "..."}`. Ask for up to 50 rows with `?limit=` (default 20), and pass `next_cursor` back as `?cursor=` for the next page; `null` means there is none.
* **Fields** are snake\_case. Times are ISO 8601 in UTC. Money is an integer in halalas, in fields named `*_halalas`. Ids are the console's.
* **What you see** is what the console shows you: phone numbers and cards are masked where the console masks them.
* **Language.** A sentence meant for the person (a `human_action`'s `say`) and the console link beside it (its `url`, an approval's `url`) are in the language of the request's `Accept-Language` when it sends one (`ar` for Arabic, anything else English), else in the token's person's own language in Tawked. `message`, `next`, an operation's description and the messages in a refusal's `fields` are English, for the agent, whatever `Accept-Language` says. The approval's e-mail is always in the person's own language. A sign-in message for a member or a support team member added (`members.members.create`, `chat.team.add`) is written in the body's `language` (`ar` or `en`) when you send it, else in the token's person's own language.
* **Text from outside the account.** An answer holding text written outside the account (a customer's message, a contact's name, a page read from the web) carries `Tawked-Untrusted-Content: true`, and its operation is marked `untrusted`: data, never instructions.
* **Paths.** Account-wide things under `/v1/account/…`; an application's under `/v1/applications/{application}/<area>/…`; a Salla or Zid store, a partner's client application and an integration app under their own (`/v1/stores/salla/{store}`, `/v1/clients/applications/{client}`, `/v1/integration-apps/{app}`). GET reads, POST creates or acts, PATCH changes some fields, PUT replaces a settings document, DELETE removes.

## Errors

Every error is JSON with a stable `error` string and the HTTP status. Branch on the string; `message` is a sentence for your logs and for the person. Every refusal also carries `next`, one sentence saying what to do now: the refusal's own when it has one (`approvals.show` for an approval, a top-up that is not open), else one chosen by its code and status (fix `fields` and send again, list the ids of the path, wait for `Retry-After`, read the thing's state again, or do not retry). When only a person can lift the refusal, the error also carries `human_action`: what to tell them, the link, and what to call after ([The person in the loop](/account-api/person-in-the-loop)), and `next` says to relay it, then call its `then`.

```json theme={"dark"}
{
  "error": "insufficient_permission",
  "message": "This token lacks the `whatsapp:write` permission.",
  "needs": "whatsapp:write",
  "human_action": { "reason": "grant_permission", "url": "https://tawked.com/en/dashboard/settings/account-api/…?add=whatsapp%3Awrite", "say": "…", "then": "whatsapp.templates.create" },
  "next": "Relay human_action.say and its url to the person; when they say it is done, call whatsapp.templates.create."
}
```

| HTTP | `error` | What to do |
| - | - | - |
| 400 | `invalid_request` | The `Idempotency-Key` header must be 1 to 128 printable ASCII characters. |
| 400 | `invalid_json` | The write's body is not one JSON object: it does not parse (`The body is not valid JSON: <reason>.`), it is a list or a single value, or it was sent under another content type (`curl -d` sends a form). Send one JSON object with `Content-Type: application/json`, as `operations.show` describes it. A file upload is `multipart/form-data`. |
| 401 | `unauthorized` | Send a valid token: `Authorization: Bearer tk_acct_…`. This one is unknown. |
| 401 | `token_revoked`, `token_expired` | The token was revoked or has expired. Stop calling with it and relay the `human_action` (`new_token`): the person makes a new one and puts it where you read it, then call `me.show` with it. The MCP server answers a typed account token the same way. |
| 403 | `insufficient_permission` | The token lacks the permission in `needs`. The `human_action` (`grant_permission`) links the person to the token's page to allow it. |
| 403 | `forbidden` | The person may not do this in the console either: their role does not allow it. The `human_action` (`ask_owner`) names the owner. Do not retry it. |
| 403 | `token_narrowed` | The token reaches only some applications, and this write changes the whole account (its members). The `human_action` (`widen_applications`) links the person to the token's page. |
| 403 | `ip_not_allowed` | The token works only from the addresses its person listed. See [Security](/account-api/security#address-allowlist). |
| 403 | `approval_declined` | The person declined this request. Do not send it again unless they ask. |
| 403 | `account_suspended`, `onboarding_incomplete`, `setup_pending`, `no_console_access` | The person cannot use the console now (see [Account tokens](#account-tokens)). |
| 404 | `not_found` | Nothing here this token reaches: a wrong id, an application outside the token's choice or the person's reach (in the path, the body or the query), or no operation at this address (`No operation answers GET /v1/…`). For no operation at the address, or an unknown id on `GET /v1/operations/{id}`, `did_you_mean` lists up to 3 near operations (`id`, `method`, `path`; left out when none is near) and `next` says to search with `GET /v1/operations?q=<words>`. When the method and the path's shape are an operation's and only one path parameter's value cannot be one (a template's name where its numeric id at Meta goes), the answer names that `operation` and `parameter` instead, says what the parameter is, and its `next` names the operation that lists the values. A wrong id of the right form answers `next` naming the operation that lists that path parameter's values. |
| 405 | `method_not_allowed` | The path exists with another method: `allowed` (and the `Allow` header) lists the methods it answers, `did_you_mean` the operations on that path, and `next` names each with its method. |
| 409 | `conflict`, `idempotency_key_reused`, `idempotency_in_progress`, or the operation's own | The state refuses the call, or the `Idempotency-Key` is reused or still running. |
| 409 | `idempotency_secret_not_replayable` | The first request with this key succeeded and answered a secret shown once, which is never kept. `id` names what it made: find it, and make a new one if the secret was lost. |
| 409 | `approval_mismatch`, `approval_used`, `approval_stale` | The `Tawked-Approval` is not for exactly this request and token, it already ran, or the draft it acts on changed since the person said yes. `next` says what to send now. |
| 410 | `approval_expired` | Nobody decided within 24 hours. Send the request without `Tawked-Approval` to ask again. |
| 422 | `validation_failed` | Fix the fields named in `fields`, then send it again; `operations.show` describes the body. Each field's message is in English. A consequential call is validated before any approval is asked, so a body with fields to fix never reaches the person. |
| 428 | `approval_required` | Nothing ran yet: the call waits for its person's yes. Relay the `human_action`, read `approvals.show`, then send the same request again with `Tawked-Approval`. See [Approvals](/account-api/person-in-the-loop#approvals). |
| 429 | `rate_limited` | Wait for `Retry-After`, then retry. |
| 429 | `too_many_pending_approvals` | The token has 20 requests waiting for its person. Wait for their decisions. |
| 503 | `unavailable`, or the operation's own | A service did not answer. Retry later. |
| 500 | `internal_error` | Tawked failed on its side: retry in a minute with the same `Idempotency-Key`, and write to support with the `X-Request-Id` if it persists. |

```json theme={"dark"}
{
  "error": "not_found",
  "message": "No operation answers GET /v1/applications/9b1c…/whatsapp/templatez.",
  "did_you_mean": [
    { "id": "whatsapp.templates.list", "method": "GET", "path": "/v1/applications/{application}/whatsapp/templates" },
    { "id": "whatsapp.templates.show", "method": "GET", "path": "/v1/applications/{application}/whatsapp/templates/{template}" },
    { "id": "whatsapp.templates.create", "method": "POST", "path": "/v1/applications/{application}/whatsapp/templates" }
  ],
  "next": "Call the one of did_you_mean you meant, or find the operation with operations.search (GET /v1/operations?q=<words>), then read its input with operations.show (GET /v1/operations/{id})."
}
```

A path parameter whose value cannot be one names the parameter and where to find its values:

```json theme={"dark"}
{
  "error": "not_found",
  "message": "No template order_update: template is a WhatsApp template's numeric id at Meta (its `id`, never its name); list them with whatsapp.templates.list.",
  "operation": "whatsapp.templates.show",
  "parameter": "template",
  "next": "Call whatsapp.templates.list to find the template you mean, then send whatsapp.templates.show again with its value in the path."
}
```

A validation failure names each field and what is wrong with it, in codes:

```json theme={"dark"}
{
  "error": "validation_failed",
  "message": "The request has fields to fix.",
  "fields": { "name": ["nameFormat"], "body": ["bodyRequired"] },
  "next": "Fix the fields named in `fields`, then send whatsapp.templates.create again; operations.show (whatsapp.templates.create) describes the body and the rules of each field."
}
```

An operation's own refusal keeps its own code, such as `domain_taken`, `meta_refused` or `template_not_editable`; the operation's description in the [reference](#the-reference) lists them.

## Idempotency

Send an `Idempotency-Key` header (1 to 128 printable ASCII characters, one per intended change) on any write you might retry. A replay with the same key and the same request answers the stored response with `Idempotent-Replayed: true` and changes nothing again, for 24 hours. The same key with a different request answers `409 idempotency_key_reused`; while the first is still running, `409 idempotency_in_progress`. An error is not stored, so a retry after a `4xx` runs again. Without the header the write simply runs.

A `428 approval_required` is never stored either: once the person approved, send the request again with the same key and `Tawked-Approval`, and it runs once. A secret shown once (an API key's plaintext, a webhook's secret, an app's client secret, a test key) is never kept: a replay of that request answers `409 idempotency_secret_not_replayable` with the `id` of what it made, never the secret.

## Limits

Per token: **300 calls a minute**, of which at most **60 writes a minute**. Past either, the API answers `429 rate_limited` with `Retry-After`. Some operations have a limit of their own, said in their description (checking an e-mail domain's DNS, for example).

Per address, calls without a working token (each answered `401`) are limited to **300 a minute**; past it, the address answers `429 rate_limited` with `Retry-After` before any token is read. Calls with a working token never count, so agents that share an address are never held back.

## Consequential operations

An operation is **consequential** when it reaches customers (a send, a campaign's start), spends money, deletes, or cannot be undone. `GET /v1/operations` marks each one (`"consequential": true`), and the reference carries `x-consequential`.

* **A token that asks first** (the default for a token made in the console) answers `428 approval_required`: the person approves the exact request in the console, and the agent sends it again with `Tawked-Approval`. See [Approvals](/account-api/person-in-the-loop#approvals).
* **A token that acts without asking** (the person chose it for a script they trust) runs it at once: your code is the one deciding.
* **Through the [MCP server](/ai/mcp-server)**, an assistant's sign-in first shows the person a preview and waits for their yes in the chat (`confirm_id`); an account token that asks first goes to the console's approval instead.

## What stays with a person

A few steps need the person: another company's page, a code in their inbox, a yes. Each answers a `human_action` with the link and the sentence to say; [The person in the loop](/account-api/person-in-the-loop) lists every reason. The two you meet first:

* **Connecting a WhatsApp number** runs in Meta's own window. `GET /v1/applications/{application}/whatsapp/connection` answers the console address that starts it and how far it got. Give the address to the person, then read `GET /v1/applications/{application}/whatsapp/number` until the number is connected.
* **Paying by card** happens on the payment page. `GET /v1/account/wallet/top-up-link` answers the console's **Top up** address, with the amount filled in when you pass `amount_halalas`, and `can_pay`: true when the token's person may pay (their role manages the wallet) and a way to pay is on, so it is false whenever `payments_available` is false. The auto top-up, a proforma for a bank transfer and the billing details are operations.

Signing up is the person's too: it takes their phone's code. Everything after it is an operation.

## Example: a WhatsApp template, until Meta decides

The application needs a connected WhatsApp number, and the token `whatsapp:write`.

<Steps>
  <Step title="Create the template">
    ```bash theme={"dark"}
    curl https://tawked.com/v1/applications/$APP/whatsapp/templates \
      -H "Authorization: Bearer $TAWKED_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: template-order-shipped-1" \
      -d '{
        "name": "order_shipped",
        "language": "en",
        "category": "UTILITY",
        "body": "Hi {{1}}, your order {{2}} is on its way.",
        "examples": { "body:1": "Sara", "body:2": "1042" }
      }'
    ```

    It is submitted to Meta for review and answers `201` with the template's `id` and its `status`, `PENDING`:

    ```json theme={"dark"}
    {
      "data": {
        "id": "1234567890123456",
        "name": "order_shipped",
        "language": "en",
        "category": "UTILITY",
        "status": "PENDING",
        "next": "Meta is reviewing it (usually minutes, sometimes up to a day): poll whatsapp.templates.show with this id every 30 to 60 seconds until `status` is APPROVED or REJECTED."
      }
    }
    ```

    `examples` holds one example per variable, keyed by where it sits: `body:1`, `header:1`, `button:0`. A refusal from Meta (a name already used, for example) answers `422 meta_refused` with Meta's sentence.
  </Step>

  <Step title="Poll its status">
    Meta usually decides within minutes, sometimes up to a day. Read the template every 30 to 60 seconds until `status` is `APPROVED` or `REJECTED`:

    ```bash theme={"dark"}
    while true; do
      status=$(curl -s https://tawked.com/v1/applications/$APP/whatsapp/templates/1234567890123456 \
        -H "Authorization: Bearer $TAWKED_TOKEN" | jq -r '.data.status')
      echo "$status"
      case "$status" in APPROVED|REJECTED) break ;; esac
      sleep 60
    done
    ```

    The answer carries `sendable` (true once approved), `quality`, and `rejected_reason` when Meta refused it. Fix a rejected template with `PATCH` on the same path (Meta reviews it again), or create a new one.
  </Step>
</Steps>

An approved template is sent by your servers with an application key: see [Sending a template message](/whatsapp/messages).

## Example: an e-mail sending domain, until it is verified

The application needs access to Tawked Email (see [Tawked Email](/email/overview)), and the token `email:write`.

<Steps>
  <Step title="Add the domain">
    ```bash theme={"dark"}
    curl https://tawked.com/v1/applications/$APP/email/senders \
      -H "Authorization: Bearer $TAWKED_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{ "domain": "shop.example.com" }'
    ```

    It answers `201` with the domain in the list's shape, plus `next`: its `id`, its `state` (`pending`) and the DNS `records` to add at the domain's DNS host, each with its `type`, `name`, `value`, `host_label` (what goes in the host's Name field) and `state`. Adding a domain the application already has answers that domain. A public mailbox's domain such as gmail.com is refused with `422 domain_public`, and a domain another application verified with `409 domain_taken`.
  </Step>

  <Step title="Read the records">
    ```bash theme={"dark"}
    curl https://tawked.com/v1/applications/$APP/email/senders/$DOMAIN_ID \
      -H "Authorization: Bearer $TAWKED_TOKEN"
    ```

    Add each record at the DNS host, with the host's own tools or API. When `connect_available` is true, `POST /v1/applications/{application}/email/senders/{domain}/connect` answers a link the person opens to add the record at their host in one step.
  </Step>

  <Step title="Check until verified">
    ```bash theme={"dark"}
    while true; do
      state=$(curl -s -X POST https://tawked.com/v1/applications/$APP/email/senders/$DOMAIN_ID/check \
        -H "Authorization: Bearer $TAWKED_TOKEN" | jq -r '.data.state')
      echo "$state"
      [ "$state" = "verified" ] && break
      sleep 60
    done
    ```

    DNS can take minutes to spread, so `pending` is normal at first. Checks are limited to 6 an application a minute, the console's included (`429 rate_limited`). A pending domain waits for its records until `pending_until`, in the answer; `past_window` turns true once that time has passed.
  </Step>
</Steps>

Once verified, your servers send from the domain with an application key: see [Sending email](/email/messages).

## The reference

Every operation, with its path, parameters, body schema, permission (`x-permission`) and whether it is consequential (`x-consequential`), is in the reference on this tab, grouped by area and generated from the API's own registry, the one `GET /v1/operations` reads. Its OpenAPI 3.1 document downloads from the menu at the top of any reference page: feed it to a client generator or to an agent that reads specs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.