API Reference · Endpoints
OTP Hub API
Send and verify WhatsApp verification codes: POST /api/v1/otp/send, POST /api/v1/otp/verify, GET /api/v1/otp/requests/:id.
The OTP Hub endpoints belong to the OTP Hub app. Install the app first, then create a key on its API keys tab. That key carries only the otp permission. A general workspace key with full access (*) also works. These endpoints don't need the API access plan feature: OTP Hub is billed per message.
| Endpoint | What it does |
|---|---|
POST /api/v1/otp/send | Send a code to a phone number |
POST /api/v1/otp/verify | Check a code the user entered |
GET /api/v1/otp/requests/:id | Read one request: status, delivery, price |
X-API-Key header on every request. Every response is JSON with success. Errors also include a machine-readable code and a readable error.Send a code
/api/v1/otp/sendGenerate a code and send it on WhatsApp| Parameter | Type | Required | Description |
|---|---|---|---|
phone | string | required | Recipient in international format with country code, e.g. 201012345678 or +966 50 123 4567. Spaces, dashes, a leading + or 00 are removed. |
language | string | optional | Message language: ar or en. Defaults to Arabic. |
test | boolean | optional | Test mode: nothing is sent or charged, and the code is returned in data.code. |
metadata | object | optional | Your own reference data (for example your user id). It is stored with the request and returned as is. |
Idempotency-Key (header) | string | optional | Up to 80 characters. A repeat within 24 hours returns the first request instead of sending again. See Safe retries. |
curl -X POST "https://inboxi.app/api/v1/otp/send" \
-H "X-API-Key: inx_live_your_otp_hub_key" \
-H "Content-Type: application/json" \
-d '{
"phone": "201012345678",
"language": "ar",
"metadata": {
"userId": "u_1842"
}
}'Response: 201 Created
{
"success": true,
"data": {
"id": "otp_5f0c2a9e41b7d3c8a6e2f019",
"to": "+2010•••••678",
"status": "pending",
"delivery": "sent",
"attempts": 0,
"attemptsLeft": 5,
"expiresAt": "2026-10-16T12:10:00.000Z",
"verifiedAt": null,
"test": false,
"billing": "wallet",
"from": "otp_hub",
"price": { "amount": 0.0104, "currency": "USD" },
"metadata": { "userId": "u_1842" },
"createdAt": "2026-10-16T12:00:00.000Z"
}
}Save data.id and pass it to /verify. In test mode the response also has "test": true,"delivery": "test", "billing": "test" and "code": "482913".
Verify a code
/api/v1/otp/verifyCheck the code the user typed| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | optional | The id returned by /send. Recommended. |
phone | string | optional | Instead of id: checks the latest code sent to this number. |
code | string | required | The code the user entered. Spaces are ignored. |
curl -X POST "https://inboxi.app/api/v1/otp/verify" \
-H "X-API-Key: inx_live_your_otp_hub_key" \
-H "Content-Type: application/json" \
-d '{
"id": "otp_5f0c2a9e41b7d3c8a6e2f019",
"code": "482913"
}'An answer is always 200 with data.valid, including for a wrong code. Allow the action only whenvalid is true. A correct code works once.
| reason | Meaning | What to do |
|---|---|---|
null | The code is correct. The request is now verified. | Continue |
invalid_code | Wrong code. attemptsLeft says how many tries remain. | Ask the user to try again |
max_attempts | Too many wrong tries. This code no longer works. | Send a new code |
expired | More than 10 minutes passed. | Send a new code |
canceled | A newer code was sent to the same number. | Use the latest code |
already_verified | This code was already used. | Treat as a new attempt |
failed | The message was not delivered. | Send a new code or offer another channel |
Get a request
/api/v1/otp/requests/:idStatus, delivery and price of one codecurl "https://inboxi.app/api/v1/otp/requests/otp_5f0c2a9e41b7d3c8a6e2f019" \
-H "X-API-Key: inx_live_your_otp_hub_key"Returns the same object as /send, without the code.
The request object
| Field | Type | Description |
|---|---|---|
id | string | Request id, starts with otp_ |
to | string | The recipient, masked |
status | string | pending · verified · expired · failed · canceled |
delivery | string | sent · delivered · read · failed · test. Updates as WhatsApp reports progress |
attempts / attemptsLeft | number | Verify tries used, and remaining |
expiresAt / verifiedAt | ISO date | When the code expires, and when it was verified |
billing | string | wallet (paid) · free_quota (monthly allowance) · admin_free (free from Inboxi) · test · none (not sent) · own_number (sent from your number, billed by Meta) |
from | string | otp_hub (the OTP Hub number) or own_number (your number). See Which number sends the codes |
price | object | { amount, currency }. What this message costs you. 0 when free or refunded |
test | boolean | Sent in test mode |
metadata | object | null | Your reference data from /send |
Errors
{
"success": false,
"code": "INSUFFICIENT_BALANCE",
"error": "Your wallet balance is too low to send this code. Top up from Settings › Billing.",
"price": 0.0104,
"balance": 0.003
}| Status | code | When |
|---|---|---|
| 400 | PHONE_REQUIRED | No phone in the body. |
| 400 | INVALID_PHONE | Not an international number (8-15 digits with country code). |
| 401 | - | Missing, invalid, inactive or expired API key. |
| 402 | INSUFFICIENT_BALANCE | The wallet cannot cover this message. The body includes price and balance. |
| 403 | - | The key does not have the otp permission (Missing permission: otp). |
| 403 | APP_NOT_INSTALLED | OTP Hub is not installed on the workspace. |
| 403 | APP_BLOCKED | OTP Hub was disabled for this workspace by INBOXI. Contact support. |
| 403 | COUNTRY_NOT_ALLOWED | The number is in a country you did not allow (OTP Hub › Settings). |
| 409 | OWN_TEMPLATE_NOT_APPROVED | You send from your own number and its verification template is not approved by Meta yet. Wait for approval, or switch to the OTP Hub number. |
| 409 | NO_WHATSAPP | You send from your own number and it is no longer connected. Reconnect it, or switch to the OTP Hub number. |
| 422 | NOT_ON_WHATSAPP | The number cannot receive WhatsApp messages. Offer another channel. |
| 429 | RESEND_TOO_SOON | Another code went to this number less than 30 seconds ago. Retry-After says when. |
| 429 | TOO_MANY_CODES | This number already received 5 codes in the last hour. |
| 429 | DAILY_LIMIT_REACHED | Your workspace reached its daily code limit (OTP Hub › Settings). Retry-After says when the day resets (UTC). |
| 429 | - | The API key went over its per-minute request limit. |
| 502 | SEND_FAILED | WhatsApp refused the message. You are not charged. |
| 503 | APP_DISABLED | OTP Hub is temporarily unavailable. |
| 503 | SENDER_UNAVAILABLE | The sending number is temporarily unavailable. Retry later. |
502 and 503 after a short wait. For 429, wait the number of seconds in theRetry-After header. Don't retry 4xx errors without changing the request.