Skip to content

WhatsApp OTP

Takeed sends login codes over WhatsApp from a verified business number. Your app keeps its own users: Takeed delivers the code and, if you want, checks it too. Your first company comes with 5 free codes.

On this page
TakeedWhatsApp

482913 is your verification code. For your security, do not share this code.

This code expires in 5 minutes.

Copy code
What your user receives by default: WhatsApp's authentication message. It can carry your own wording instead (see below).

Before you start#

  1. Create an account, enter your company name, then add an app in the dashboard.
  2. Open Settings → OTP settings → API authorization and create a key. This is the secret used in Authorization: Key <key>; it is shown in full once. With this application key, target is optional unless you want to select one branch.
  3. In Message credit, transfer OTP credit from the company wallet to this app. A new app starts at zero and cannot send until it is funded.
  4. Keep the key on your server. Never put it in an app or a web page.

Deliver mode (default)#

Your backend makes the code and checks it; Takeed only delivers it.

bash
curl https://takeed-iq.com/api/v1/otp/send \
  -H "Authorization: Key $TAKEED_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone": "+9647701234567", "code": "482913", "user_ip": "203.0.113.7"}'

Takeed answers 202 Accepted straight away and sends in the background:

json
{
  "id": "01JAB3K9Q7X2M4N8P6R5T0VWYZ",
  "mode": "deliver",
  "status": "queued",
  "phone": "+964•••••••567",
  "target": "shop",
  "verified": false,
  "expires_at": null,
  "error": null,
  "created_at": "2026-10-15T09:41:00+00:00",
  "sent_at": null,
  "delivered_at": null,
  "read_at": null,
  "verified_at": null
}

The code itself is never stored. Keep the id to follow the code.

API timestamps are ISO 8601 in UTC (+00:00) so integrations are unambiguous. The dashboard displays them in Baghdad time (Asia/Baghdad).

Managed mode#

Takeed makes the code (6 digits, valid 5 minutes, 5 attempts by default), sends it and checks it. Turn it on in the dashboard: open the app, then the OTP tab, then Managed mode.

  1. Send without code:

    bash
    curl https://takeed-iq.com/api/v1/otp/send \
      -H "Authorization: Key $TAKEED_KEY" \
      -H "Content-Type: application/json" \
      -d '{"phone": "+9647701234567", "user_ip": "203.0.113.7"}'
  2. The 202 response has "mode": "managed", an id and an expires_at. Keep the id with the sign-in attempt.

  3. When the user types the code, send it with the id:

    bash
    curl https://takeed-iq.com/api/v1/otp/verify \
      -H "Authorization: Key $TAKEED_KEY" \
      -H "Content-Type: application/json" \
      -d '{"id": "01JAB3K9Q7X2M4N8P6R5T0VWYZ", "code": "482913"}'

A right code answers 200 with "status": "verified". Otherwise the answer names the reason: code_invalid (wrong code), too_many_attempts, code_expired or already_verified. Every reason is in Error codes.

Fields#

Field Required Notes
phone Yes E.164, e.g. +9647701234567
user_ip Yes The end user's public IP, forwarded by your backend. A private, reserved, or your own server's IP is refused (invalid_ip).
target No Use "app:branch" to select a branch. The application key already selects its app.
code Deliver mode only 4–10 letters or digits. Leave it out in managed mode.
user_agent No Stored with the code, to help trace abuse.
device_id No Stored with the code, to help trace abuse.

Following a code#

Poll GET /api/v1/otp/{id}, or let Takeed call your webhook each time the code moves on:

EventStateMeaning
otp.sentsentWhatsApp accepted the message. One credit is used.
otp.delivereddeliveredThe message reached the phone.
otp.readreadThe user opened the chat.
otp.failedfailedWhatsApp could not deliver it; error says why. The credit is given back.
otp.verifiedverifiedManaged mode: the user typed the right code.
otp.expiredexpiredManaged mode: the code ran out before it was used.

Webhook#

Set the URL (HTTPS) in the dashboard: the app's General tab, Webhook URL. Create the signing secret on the Security tab. Each event is a POST with this body, where data.otp has every field of the API response above (a few are shown here):

json
{
  "event": "otp.delivered",
  "created_at": "2026-10-15T09:41:03+00:00",
  "data": {
    "otp": {"id": "01JAB3K9Q7X2M4N8P6R5T0VWYZ", "mode": "deliver", "status": "delivered", "target": "shop"}
  }
}

Each request carries a signature header in this form:

X-Takeed-Signature: t=<unix time>,v1=<HMAC-SHA256 of "<t>.<body>" with the webhook secret>

Check it, and reject old timestamps:

php
// $header: the X-Takeed-Signature value. $body: the raw request body.
[$t, $v1] = sscanf($header, 't=%d,v1=%s');
$expected = hash_hmac('sha256', $t.'.'.$body, $secret);
$ok = hash_equals($expected, (string) $v1) && abs(time() - $t) < 300;

Protection#

Takeed enforces, per app (adjustable on the app's OTP tab):

  • per phone: 60 seconds between codes, 5 per hour;
  • per IP: 30 per hour; after 10 refused requests in an hour the IP is blocked for the rest of the hour;
  • per app: 100 per minute;
  • a country-code allowlist.

A refused request answers with its reason (phone_rate_limited, ip_blocked, blocked_country…), and the dashboard lists it with the same reason.

Your own wording#

By default the message is WhatsApp's authentication text shown above. It can say what you want instead: your brand, the branch's name, the minutes left. Ask the Takeed team for wording for your company, one app or one branch; it can use {code}, {name} (the branch, or the app) and {minutes}. WhatsApp approves each wording before it is used, and the most specific one wins: branch, then app, then company.

Credit#

Each code WhatsApp accepts uses one credit from the app's OTP balance, whether or not the user verifies it. A provider failure refunds the same app. When the app balance is empty, requests are refused with application_credit_exhausted (402); transfer more from Message credit. If the company wallet has no distributable balance, its administrator must obtain credit before funding the app. Notifications never use this balance.

Read the available OTP messages#

Your backend can read the application's current OTP balance with the same Authorization key used for sending:

bash
curl https://takeed-iq.com/api/v1/otp/balance \
  -H "Authorization: Key $TAKEED_KEY"
json
{
  "object": "otp_balance",
  "app_id": "971ea6e3-2f83-4a5c-a501-1f7a285d06d5",
  "target": "my-app",
  "available_messages": 1000
}

available_messages is the number of OTP messages left in this application's wallet at the moment of the request. It is a live snapshot and can change as concurrent sends are accepted or refunded. One WhatsApp-accepted OTP consumes one message; application notifications are free and do not change this number. Do not expose this endpoint or the Authorization key in browser or mobile code: call it only from your backend.

An application key needs no parameters. A legacy company-wide key must select an application with ?target=my-app; ?target=my-app:north is also accepted, but branches share their application's OTP wallet. The response is sent with Cache-Control: private, no-store and counts toward the API key's normal rate limit.

Try it on your own number: your first organization comes with 5 free WhatsApp codes.

Create account