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

# Partner quickstart

> From a partner account to a client application, a test code on your own phone and a signed webhook, in code, before anything is approved.

At the end you will have one client application for a test merchant, a test code delivered to your phone and checked, and your webhook receiving the signed event. Nothing here needs Tawked's approval: the sandbox key sends while the application is still in review.

<Prompt description="Hand this to your coding agent to wire the Tawked Partner API into your platform." icon="robot" actions={["copy", "cursor"]}>
  Integrate the Tawked Partner API into this platform: one Tawked Verify application per merchant, addressed by our own merchant id.
  Read [https://docs.tawked.com/partners/overview](https://docs.tawked.com/partners/overview) and [https://tawked.com/openapi.json](https://tawked.com/openapi.json) for every request shape and error code; do not guess.

  * Read the live partner key from TAWKED\_PARTNER\_KEY and the sandbox key from TAWKED\_PARTNER\_TEST\_KEY, on the server only. Never send either to a browser or a mobile app.
  * When a merchant joins, call POST [https://tawked.com/v1/partner/applications](https://tawked.com/v1/partner/applications) with external\_id (our merchant id), name\_ar, name\_en and website, and an Idempotency-Key header. Store the returned status.
  * Send codes with POST [https://tawked.com/v1/verify/start](https://tawked.com/v1/verify/start) and check them with POST [https://tawked.com/v1/verify/check](https://tawked.com/v1/verify/check), passing application (the external\_id) in every body.
  * Receive webhooks: verify tawked-signature (hex HMAC-SHA256 of the tawked-timestamp header, a dot and the raw body, with the signing secret) before parsing, answer 2xx within 5 seconds, and update the merchant on service.approved and service.rejected.
  * Branch on the error field of every error, never on the HTTP status.
</Prompt>

**Before you start**

* A partner account. Tawked opens them: write to [support@tawked.com](mailto:support@tawked.com).
* Access to **Clients** with manage rights. The owner always has it.
* The phone the account owner verified at signup, in your hand: test codes reach only that number.

<Steps>
  <Step title="Create your two keys">
    In the console, open **Clients → API and webhook**.

    * **Partner key**, **Create the key**: the live `tk_partner_` key. It provisions and changes applications, and sends real codes.
    * **Sandbox key**, **Create a sandbox key**: the `tk_partner_test_` key. It reads everything and sends to the owner's phone only, with the `[TEST]` stamp. It changes nothing.

    Each key is shown once. Put them in your server's environment:

    ```bash .env theme={"dark"}
    TAWKED_PARTNER_KEY=tk_partner_xxxxxxxxxxxxxxxxxxxx
    TAWKED_PARTNER_TEST_KEY=tk_partner_test_xxxxxxxxxxxxxxxxxxxx
    ```
  </Step>

  <Step title="Point the webhook at your server">
    Still on **Clients → API and webhook**, **Webhook**, **Add the URL**: an `https://` address on a public host. Keep the **Signing secret** beside your keys.

    Then subscribe to the code outcomes as well as the defaults, so the test in step 5 reaches you:

    <CodeGroup>
      ```bash cURL theme={"dark"}
      curl -X PUT https://tawked.com/v1/partner/webhook \
        -H "Authorization: Bearer $TAWKED_PARTNER_KEY" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: webhook-events-1" \
        -d '{ "events": ["service.approved", "service.rejected", "service.review_requested", "service.suspended", "service.paused", "balance.low", "verification.verified"] }'
      ```

      ```javascript Node.js theme={"dark"}
      const res = await fetch('https://tawked.com/v1/partner/webhook', {
        method: 'PUT',
        headers: {
          Authorization: `Bearer ${process.env.TAWKED_PARTNER_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': 'webhook-events-1',
        },
        body: JSON.stringify({
          events: ['service.approved', 'service.rejected', 'service.review_requested', 'service.suspended', 'service.paused', 'balance.low', 'verification.verified'],
        }),
      });
      ```

      ```php PHP theme={"dark"}
      use Illuminate\Support\Facades\Http;

      $res = Http::withToken(getenv('TAWKED_PARTNER_KEY'))
          ->withHeaders(['Idempotency-Key' => 'webhook-events-1'])
          ->put('https://tawked.com/v1/partner/webhook', [
              'events' => ['service.approved', 'service.rejected', 'service.review_requested', 'service.suspended', 'service.paused', 'balance.low', 'verification.verified'],
          ]);
      ```

      ```python Python theme={"dark"}
      import os, requests

      res = requests.put(
          "https://tawked.com/v1/partner/webhook",
          headers={"Authorization": f"Bearer {os.environ['TAWKED_PARTNER_KEY']}", "Idempotency-Key": "webhook-events-1"},
          json={"events": ["service.approved", "service.rejected", "service.review_requested", "service.suspended", "service.paused", "balance.low", "verification.verified"]},
      )
      ```
    </CodeGroup>

    The answer is the configuration, with `events` as you set them. [Partner events](/partners/events) lists every event and the defaults.
  </Step>

  <Step title="Provision a test merchant">
    Create the application with the **live** key, with your own id for the merchant as `external_id`:

    <CodeGroup>
      ```bash cURL theme={"dark"}
      curl -X POST https://tawked.com/v1/partner/applications \
        -H "Authorization: Bearer $TAWKED_PARTNER_KEY" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: provision-store_88" \
        -d '{ "external_id": "store_88", "name_ar": "متجر الورد", "name_en": "Flower Shop", "website": "https://flowers.example" }'
      ```

      ```javascript Node.js theme={"dark"}
      const res = await fetch('https://tawked.com/v1/partner/applications', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.TAWKED_PARTNER_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': 'provision-store_88',
        },
        body: JSON.stringify({ external_id: 'store_88', name_ar: 'متجر الورد', name_en: 'Flower Shop', website: 'https://flowers.example' }),
      });
      const application = await res.json();
      ```

      ```php PHP theme={"dark"}
      $application = Http::withToken(getenv('TAWKED_PARTNER_KEY'))
          ->withHeaders(['Idempotency-Key' => 'provision-store_88'])
          ->post('https://tawked.com/v1/partner/applications', [
              'external_id' => 'store_88',
              'name_ar' => 'متجر الورد',
              'name_en' => 'Flower Shop',
              'website' => 'https://flowers.example',
          ])
          ->json();
      ```

      ```python Python theme={"dark"}
      res = requests.post(
          "https://tawked.com/v1/partner/applications",
          headers={"Authorization": f"Bearer {os.environ['TAWKED_PARTNER_KEY']}", "Idempotency-Key": "provision-store_88"},
          json={"external_id": "store_88", "name_ar": "متجر الورد", "name_en": "Flower Shop", "website": "https://flowers.example"},
      )
      application = res.json()
      ```
    </CodeGroup>

    ```json 201 Created 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
    }
    ```

    `status` is `review` while a decision is pending, or `active` at once under the auto-clean approval mode. `screening.rules` lists what the names tripped, if anything. A second call with the same `external_id` answers `200` with the application as it stands, so a retry never makes two.

    Likely errors: [`400 invalid_request`](/errors#partner-api) with `fields` naming what was refused, and [`403 live_key_required`](/errors#partner-api) if you used the sandbox key.
  </Step>

  <Step title="Send a test code to your phone">
    Now the **sandbox** key, with `application` naming the merchant and `to` the owner's verified phone:

    <CodeGroup>
      ```bash cURL theme={"dark"}
      curl -X POST https://tawked.com/v1/verify/start \
        -H "Authorization: Bearer $TAWKED_PARTNER_TEST_KEY" \
        -H "Content-Type: application/json" \
        -H "Idempotency-Key: test-store_88-1" \
        -d '{ "to": "0551234567", "lang": "ar", "application": "store_88", "reference": "quickstart" }'
      ```

      ```javascript Node.js theme={"dark"}
      const res = await fetch('https://tawked.com/v1/verify/start', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.TAWKED_PARTNER_TEST_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': 'test-store_88-1',
        },
        body: JSON.stringify({ to: '0551234567', lang: 'ar', application: 'store_88', reference: 'quickstart' }),
      });
      const { id } = await res.json();
      ```

      ```php PHP theme={"dark"}
      $id = Http::withToken(getenv('TAWKED_PARTNER_TEST_KEY'))
          ->withHeaders(['Idempotency-Key' => 'test-store_88-1'])
          ->post('https://tawked.com/v1/verify/start', [
              'to' => '0551234567',
              'lang' => 'ar',
              'application' => 'store_88',
              'reference' => 'quickstart',
          ])
          ->json('id');
      ```

      ```python Python theme={"dark"}
      res = requests.post(
          "https://tawked.com/v1/verify/start",
          headers={"Authorization": f"Bearer {os.environ['TAWKED_PARTNER_TEST_KEY']}", "Idempotency-Key": "test-store_88-1"},
          json={"to": "0551234567", "lang": "ar", "application": "store_88", "reference": "quickstart"},
      )
      verification_id = res.json()["id"]
      ```
    </CodeGroup>

    ```json 201 Created theme={"dark"}
    { "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44", "status": "pending", "expires_at": "2026-09-01T12:34:56.789Z" }
    ```

    The SMS goes out under Tawked's sender ID with the `[TEST]` stamp. It names the merchant's application when screening found its names clean, and a neutral test name otherwise. The sandbox key sends 20 codes per application and 200 per account a day (Riyadh time), each charged like a live send.

    Likely errors: [`422 sandbox_unverified_destination`](/errors#verify) for any other number, [`429 sandbox_quota_exceeded`](/errors#verify) past the daily caps, and [`404 service_not_found`](/errors#shared-by-every-endpoint) for an `external_id` you have not provisioned.
  </Step>

  <Step title="Check the code">
    Send the code you received, with the same `application`:

    <CodeGroup>
      ```bash cURL theme={"dark"}
      curl -X POST https://tawked.com/v1/verify/check \
        -H "Authorization: Bearer $TAWKED_PARTNER_TEST_KEY" \
        -H "Content-Type: application/json" \
        -d '{ "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44", "code": "482913", "application": "store_88" }'
      ```

      ```javascript Node.js theme={"dark"}
      const res = await fetch('https://tawked.com/v1/verify/check', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.TAWKED_PARTNER_TEST_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ id, code: '482913', application: 'store_88' }),
      });
      const { verified, status } = await res.json();
      ```

      ```php PHP theme={"dark"}
      $result = Http::withToken(getenv('TAWKED_PARTNER_TEST_KEY'))
          ->post('https://tawked.com/v1/verify/check', [
              'id' => $id,
              'code' => '482913',
              'application' => 'store_88',
          ])
          ->json();
      ```

      ```python Python theme={"dark"}
      res = requests.post(
          "https://tawked.com/v1/verify/check",
          headers={"Authorization": f"Bearer {os.environ['TAWKED_PARTNER_TEST_KEY']}"},
          json={"id": verification_id, "code": "482913", "application": "store_88"},
      )
      result = res.json()
      ```
    </CodeGroup>

    ```json 200 OK theme={"dark"}
    { "verified": true, "status": "verified" }
    ```

    A wrong code answers `"verified": false` with `status: "invalid_code"` and `attempts_remaining`. Branch on `verified`.
  </Step>

  <Step title="Receive the webhook">
    The check fires `verification.verified` to your webhook, with `mode: "partner_test"` and your `external_id` in `data`:

    ```json theme={"dark"}
    {
      "event": "verification.verified",
      "data": {
        "id": "b7e5c2b0-9c1a-4e2f-8f2a-3a6b0e9d1c44",
        "status": "verified",
        "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
        "reference": "quickstart",
        "channel": "sms",
        "to": "+966551234567",
        "mode": "partner_test",
        "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",
        "external_id": "store_88"
      },
      "sent_at": "1788265862201"
    }
    ```

    Verify `tawked-signature` against the raw body with your signing secret; the code is on [Webhooks](/webhooks#verifying-the-signature). Answer `2xx` within 5 seconds. **Delivery log** on **Clients → API and webhook** shows the attempt and your server's answer.

    If you do not want one event per code in production, drop `verification.verified` from `events` later.
  </Step>
</Steps>

## Go live

1. **Wait for the decision.** `service.approved` reaches your webhook when the application is approved, by you in **Clients → Reviews** or by Tawked, according to your approval mode. [`GET /v1/partner/profile`](/api-reference/partner/get-the-profile) says which mode you have. `service.rejected` carries a `reason_code`; fix the brand and `PATCH` it to resubmit.
2. **Send with the live key.** Swap `TAWKED_PARTNER_TEST_KEY` for `TAWKED_PARTNER_KEY` in the Verify calls. Codes then reach any Saudi mobile and are charged to your balance at your price.
3. **Keep the balance up.** A send with an empty balance answers `402 insufficient_credits`. `balance.low` warns you first; set its threshold on **Clients → API and webhook**, **Webhook**.
4. **Pass `client_ip` and an `Idempotency-Key`** on every `start`, so the per-address cap and safe retries protect you. See [Reliability and limits](/reliability).

<CardGroup cols={2}>
  <Card title="Applications" icon="layer-group" href="/partners/applications">
    Bulk provisioning, brand updates, verify settings, suspend and archive.
  </Card>

  <Card title="Sending codes for an application" icon="paper-plane" href="/partners/sending">
    What blocks a send, the code log and the sandbox key in full.
  </Card>

  <Card title="Events" icon="list-check" href="/partners/events">
    Every event, the rejection codes, and the feed to poll.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/errors#partner-api">
    Every code, what to do and whether to retry.
  </Card>
</CardGroup>


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