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

# Add a menu

> For a connected app's key alone, with `chat:menus` in its connection's grant: an application's own key, full access included, answers `403 insufficient_scope`, and adds its menus in the console. Adds a conversation menu to the application, under the console's own rules, and answers it. Tawked checks whether the site may be framed inside Tawked Chat and answers it in `frame`: `opens` (framed in the conversation), `window` (the site refuses frames: agents get "Open in a new window") or `unknown` (Tawked could not tell). The client's team sees the tab after reloading Tawked Chat. Put a key made for this one client in the link, never your Tawked key or client secret. A refused field answers `400 invalid_request` with `fields` naming it and `errors` giving its codes.

Guide: [Connected apps](/integrations/connected-apps)


## OpenAPI

````yaml https://tawked.com/openapi.json post /v1/chat/menus
openapi: 3.1.0
info:
  title: Tawked API
  version: 2026-09
  summary: >-
    Tawked Verify: OTP verification for Saudi mobile numbers over SMS, in two
    calls. Tawked Notifications: WhatsApp template messages from the
    application's own number. Tawked Email: transactional e-mail from the
    application's own verified domain. Tawked Chat: the conversation menus a
    connected app adds beside the conversations of its connection's application,
    and the auto-attendant it sets up there. The Partner API for platforms that
    run Tawked Verify for their merchants.
  description: >-
    The public API of Tawked, a Saudi messaging platform: Tawked Verify
    (one-time codes), Tawked Notifications (WhatsApp templates from the
    application's own number), Tawked Email (transactional e-mail from the
    application's own domain) and the Partner API. Tawked Verify is an OTP
    verification API for Saudi numbers. One call sends a one-time code by SMS to
    a Saudi mobile number, and a second call checks it. SAR 0.09 per SMS code
    sent, prepaid; a send the network refuses is refunded. Authenticate with a
    Bearer API key from the dashboard (tk_live_ for production, tk_test_ for the
    sandbox, tk_partner_ for partners, tk_partner_test_ for the partner
    sandbox). Partners (tk_partner_) provision a Verify application per merchant
    under /v1/partner/applications and name it as application on every Verify
    call. Both /v1/... and /api/v1/... answer identically. Rate limit: 120
    requests per minute per key.
  termsOfService: https://tawked.com/en/terms
  license:
    name: Proprietary (Tawked terms of service)
    url: https://tawked.com/en/terms
  contact:
    name: Tawked support
    email: support@tawked.com
    url: https://tawked.com/en/contact
servers:
  - url: https://tawked.com
    description: Production (the sandbox is a tk_test_ key on the same host)
  - url: https://tawked.com/api
    description: The same API under /api/v1/..., kept for older integrations
security:
  - bearerKey: []
  - headerKey: []
tags:
  - name: Verify
    x-group: Verify
    description: Send and check one-time codes.
  - name: WhatsApp
    x-group: Notifications
    description: >-
      Tawked Notifications: send approved WhatsApp templates through the number
      connected to an application. An application in review or approved connects
      its own number from the dashboard.
  - name: Email
    x-group: Email
    description: >-
      Tawked Email: send one transactional message per call from an address on a
      verified domain of the application, and read its status. Live keys only.
  - name: Chat
    x-group: Chat
    description: >-
      Tawked Chat: the conversation menus a connected app adds beside the
      conversations of its connection's application, and reads, changes and
      removes, with `chat:menus`; and the application's auto-attendant, which a
      connected app sets up, publishes and switches on or off, with
      `chat:attendant`. A connected app's key only; a business adds its own
      menus and builds its auto-attendant in the console.
  - name: Partner
    x-group: Partner API
    description: >-
      For platforms that run Tawked Verify for their merchants: provision a
      Verify application per merchant, keep its brand and verify settings
      current, switch it off and on, and read usage and the code log. Partner
      keys (tk_partner_) only; every Verify operation then takes `application`
      with the partner's own id for the application.
  - name: Webhooks
    x-group: Webhooks
    description: >-
      The events Tawked sends to your endpoint: an application's verification
      outcomes and message statuses, and a partner account's lifecycle, balance
      and opt-in verification events. Every delivery is signed the same way and
      retried the same way.
externalDocs:
  description: Developer docs, with code samples
  url: https://docs.tawked.com
paths:
  /v1/chat/menus:
    post:
      tags:
        - Chat
      summary: Add a menu
      description: >-
        For a connected app's key alone, with `chat:menus` in its connection's
        grant: an application's own key, full access included, answers `403
        insufficient_scope`, and adds its menus in the console. Adds a
        conversation menu to the application, under the console's own rules, and
        answers it. Tawked checks whether the site may be framed inside Tawked
        Chat and answers it in `frame`: `opens` (framed in the conversation),
        `window` (the site refuses frames: agents get "Open in a new window") or
        `unknown` (Tawked could not tell). The client's team sees the tab after
        reloading Tawked Chat. Put a key made for this one client in the link,
        never your Tawked key or client secret. A refused field answers `400
        invalid_request` with `fields` naming it and `errors` giving its codes.
      operationId: chat_menu_create
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: >-
            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.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  description: >-
                    The name your client's team sees on the tab: 2 to 30
                    characters, unique among the menus that show in the
                    application's conversations. Refused: `required`, `length`,
                    `taken`.
                url:
                  type: string
                  description: >-
                    The link of the page, saved as sent: `https://` only, at
                    most 1024 characters, on a public domain name (no IP
                    address, no port but 443, no user name or password, no token
                    in the host). It may hold the tokens `{customer_phone}`,
                    `{customer_name}`, `{customer_email}` and
                    `{conversation_id}`, filled when the menu opens, and the key
                    you made for this one client. Refused: `required`, `http`,
                    `host`, `length`, `reserved`.
                icon:
                  type:
                    - string
                    - object
                  description: >-
                    `site` by default: `"site"` takes the link's site's own icon
                    (its favicon or apple-touch-icon), as the console does;
                    `"none"` shows no icon; `{ "url": "https://..." }` is an
                    https link to a PNG, JPEG or WebP of at most 1 MB and at
                    least 32 px on its short side, which Tawked downloads once
                    from a public address and turns into the 64 px icon Tawked
                    Chat shows. Refused: `type` (not a PNG, JPEG or WebP of at
                    most 1 MB, or an `icon` of another shape), `small` (under 32
                    px), `unreachable` (the link could not be read).
              required:
                - title
                - url
      responses:
        '201':
          description: Added
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  title:
                    type: string
                  url:
                    type: string
                  icon_url:
                    type: string
                  frame:
                    type: string
                  created_at:
                    type: string
                    format: date-time
                  updated_at:
                    type: string
                    format: date-time
              examples:
                added:
                  summary: Added
                  value:
                    id: 5b1d2c3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e
                    title: الحجوزات
                    url: >-
                      https://app.yourplatform.example/tawked/bookings?key=PER_CLIENT_KEY&phone={customer_phone}
                    icon_url: >-
                      https://tawked.com/storage/chat-menus/5b1d2c3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e.png
                    frame: opens
                    created_at: '2026-10-15T09:00:00Z'
                    updated_at: '2026-10-15T09:00:00Z'
        '400':
          description: >-
            Refused fields / `invalid_request`: `fields` names every refused
            field and `errors` gives its codes. `title`: `required`, `length`
            (not 2 to 30 characters), `taken` (another menu of the application
            has that name). `url`: `required`, `http` (not `https://`), `host`
            (an IP address, a port but 443, a user name or password, a private
            or local name, a token in the host), `length` (over 1024
            characters), `reserved` (a link of Tawked's own). `icon`: `type`
            (not a PNG, JPEG or WebP of at most 1 MB, or an `icon` of another
            shape), `small` (under 32 px on its short side), `unreachable` (the
            icon's link could not be read). An edit with none of `title`, `url`
            and `icon` answers `title` `required`. Also a malformed
            `Idempotency-Key`.; `invalid_json`: The request body is not valid
            JSON.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - properties:
                      error:
                        enum:
                          - invalid_request
                          - invalid_json
              examples:
                refused_fields:
                  summary: Refused fields
                  value:
                    error: invalid_request
                    fields:
                      - title
                    errors:
                      title:
                        - taken
        '401':
          description: >-
            `unauthorized`: Missing, unknown, revoked, or mode-mismatched key;
            or an unknown/suspended partner.; `key_expired`: The key's
            `expires_at` has passed. Checked right after the key is recognised,
            before the IP allowlist or scope.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - properties:
                      error:
                        enum:
                          - unauthorized
                          - key_expired
        '403':
          description: >-
            `live_key_required`: A test key. A connected app's key is live; use
            the key the exchange answered.; `account_not_active`: The
            application is not approved yet, the account is banned, or a partner
            disabled this application. All four causes share this one response.;
            `ip_not_allowed`: The caller's IP does not match the key's
            `ip_allowlist` (exact IPs and/or CIDR ranges, IPv4 and IPv6).;
            `insufficient_scope`: The 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. This operation needs `chat:menus`. An application's
            own key answers this, full access included: only a connected app's
            key holds that scope.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - properties:
                      error:
                        enum:
                          - live_key_required
                          - account_not_active
                          - ip_not_allowed
                          - insufficient_scope
          headers:
            WWW-Authenticate:
              description: >-
                Sent with `insufficient_scope` only: the scope this operation
                needs, which the key was not granted.
              schema:
                type: string
              example: Bearer error="insufficient_scope", scope="chat:menus"
        '409':
          description: >-
            Tawked Chat is not ready / `idempotency_in_progress`: A request with
            this `Idempotency-Key` is still running. Wait for its answer, or
            retry in a moment.; `idempotency_key_reused`: This `Idempotency-Key`
            was already used for a different request. Use one key per change.;
            `chat_not_ready`: `reason` says why: `chat`, the client's account
            has no Tawked Chat yet; `number`, the application's WhatsApp number
            is not in Tawked Chat yet. Nothing changed. Tell the client, and try
            again once it is ready.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - properties:
                      error:
                        enum:
                          - idempotency_in_progress
                          - idempotency_key_reused
                          - chat_not_ready
              examples:
                tawked_chat_is_not_ready:
                  summary: Tawked Chat is not ready
                  value:
                    error: chat_not_ready
                    reason: number
        '422':
          description: >-
            Ten menus already / `menu_limit_reached`: The application already
            has 10 menus, counting the client's own and every app's. `limit`
            says how many. Nothing was added.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - properties:
                      error:
                        enum:
                          - menu_limit_reached
              examples:
                ten_menus_already:
                  summary: Ten menus already
                  value:
                    error: menu_limit_reached
                    limit: 10
        '429':
          description: >-
            `too_many_requests`: Per-key rate limit exceeded (default 120
            requests/minute, fixed 60s window). Checked before authentication.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - properties:
                      error:
                        enum:
                          - too_many_requests
        '500':
          description: >-
            `internal_error`: An unhandled server error. Quote the X-Request-Id
            header if you report it.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - properties:
                      error:
                        enum:
                          - internal_error
        '503':
          description: >-
            `chat_unavailable`: Tawked Chat did not answer. Nothing changed;
            retry with the same `Idempotency-Key`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Error'
                  - properties:
                      error:
                        enum:
                          - chat_unavailable
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |-
            curl -X POST https://tawked.com/v1/chat/menus \
              -H "Authorization: Bearer tk_live_xxxxxxxxxxxxxxxxxxxx" \
              -H "Content-Type: application/json" \
              -H "Idempotency-Key: client-8841-bookings-menu" \
              -d '{
                "title": "الحجوزات",
                "url": "https://app.yourplatform.example/tawked/bookings?key=PER_CLIENT_KEY&phone={customer_phone}",
                "icon": "site"
            }'
        - lang: javascript
          label: Node.js
          source: |-
            const res = await fetch("https://tawked.com/v1/chat/menus", {
              method: "POST",
              headers: {
                Authorization: "Bearer tk_live_xxxxxxxxxxxxxxxxxxxx",
                "Content-Type": "application/json",
                "Idempotency-Key": "client-8841-bookings-menu",
              },
              body: JSON.stringify({
                  "title": "الحجوزات",
                  "url": "https://app.yourplatform.example/tawked/bookings?key=PER_CLIENT_KEY&phone={customer_phone}",
                  "icon": "site"
              }),
            });
            const data = await res.json();
        - lang: python
          label: Python
          source: |-
            import requests

            res = requests.post(
                "https://tawked.com/v1/chat/menus",
                headers={"Authorization": "Bearer tk_live_xxxxxxxxxxxxxxxxxxxx", "Idempotency-Key": "client-8841-bookings-menu"},
                json={
                    "title": "الحجوزات",
                    "url": "https://app.yourplatform.example/tawked/bookings?key=PER_CLIENT_KEY&phone={customer_phone}",
                    "icon": "site"
                },
            )
            data = res.json()
components:
  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
          description: A stable machine-readable code; the docs list every one.
      additionalProperties: true
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      description: >-
        Authorization: Bearer <api key>. An application key (tk_live_, tk_test_)
        has full access, which covers every product and any product added later,
        or custom access, chosen when the key is created: one or more of the
        scopes `verify:check`, `verify:send` (includes `verify:check`),
        `notifications:read`, `notifications:send` (includes
        `notifications:read`), `email:read`, `email:send` (includes
        `email:read`), `notifications:templates` (includes
        `notifications:read`), `chat:menus`, `chat:attendant`. No key made on
        the API keys page holds `notifications:templates`, full access included:
        the three template writes answer such a key 403 `insufficient_scope`. A
        connected app's key holds it. Nor `chat:menus`: the five conversation
        menu operations answer such a key 403 `insufficient_scope`, and a
        connected app's key holds it once the client approved it. Nor
        `chat:attendant`: the five auto-attendant operations answer such a key
        403 `insufficient_scope` too, and a connected app's key holds it once
        the client approved it. A call outside the key's access answers 403
        `insufficient_scope` and names the scope it needs in the
        `WWW-Authenticate` response header. A partner key (tk_partner_) always
        has full access. Whatever its access, a key is a server-side secret.
    headerKey:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        The same API key, with the same access, as a header, for clients that
        cannot set Authorization.

````

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