Introduction
Toome Developer API is a REST API that lets any external system integrate with the Toome platform. You can use it to send WhatsApp notifications, receive orders from external stores, and manage customer data in the CRM — all authenticated through a secure Bearer Token issued to each registered application.
https://toome.cloud/api/v1All requests and responses are in JSON. Every request must include the headers
Content-Type: application/json and Accept: application/json.
Authentication
The API uses the OAuth2 Client Credentials flow, the pattern designed for server-to-server connections where no end user is involved in the authentication process. Steps to get started:
- Register a new application from the Developer Portal to get your
client_idandclient_secret. - Send a
POST /api/v1/tokenrequest with these credentials to obtain anaccess_token. - Attach the access token to every subsequent request in the header:
Authorization: Bearer {token}.
401 Unauthorized, and you must issue a new one.Scopes
Each application is granted a specific set of scopes when it's created. No request can exceed the scopes assigned to it.
| Scope | Grants |
|---|---|
| notify | Send WhatsApp notifications via Meta-approved message templates |
| send_notifications | Send WhatsApp order-status notifications (confirmed, shipped, delivered...) via Toome's built-in templates — consumes credits per message |
| orders.write | Create external orders and update their status in Toome |
| orders.read | Retrieve and view stored orders |
| customers.write | Add new customers and edit their data in the CRM |
| customers.read | Retrieve customer data with search and filtering support |
| brain.chat | Interact with the AI engine linked to your account |
Error Codes
The API returns standard HTTP status codes along with a JSON body containing error details.
| HTTP Code | Meaning |
|---|---|
| 401 | Access token missing or expired — issue a new token |
| 402 | INSUFFICIENT_CREDITS — account balance is not enough to complete the operation — check balance and required in the response |
| 403 | The application doesn't have the required scope, or the feature is disabled (FEATURE_DISABLED) — contact support to enable it |
| 404 | The requested resource doesn't exist (e.g. an undefined message template) |
| 422 | Validation error on the submitted data — check the errors field in the response |
| 429 | You've exceeded the allowed request rate — wait before retrying |
| 502 | Message delivery failed via the Meta API — may be a temporary WhatsApp-side issue |
POST /token
Issue an access token using the application's credentials. This token is used in the
Authorization header for all subsequent API requests.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| client_id | string | required | Your application's unique identifier — starts with tm_ |
| client_secret | string | required | Your application's secret key — keep it in environment variables, never in code |
curl -X POST https://toome.cloud/api/v1/token \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "client_id": "tm_xxxxxxxxxxxxxxxx", "client_secret": "your_secret_here" }'
$response = Http::post('https://toome.cloud/api/v1/token', [ 'client_id' => 'tm_xxxxxxxxxxxxxxxx', 'client_secret' => 'your_secret_here', ]); $token = $response->json('access_token');
import requests res = requests.post('https://toome.cloud/api/v1/token', json={ 'client_id': 'tm_xxxxxxxxxxxxxxxx', 'client_secret': 'your_secret_here' }) token = res.json()['access_token']
{ "access_token": "...", "token_type": "Bearer", "scopes": ["notify","orders.write"] }{ "error": "invalid_client", "message": "Invalid credentials" }GET /test-connection
Verify that an access token is valid and inspect the scopes granted to the application. Useful for diagnostics and confirming a successful integration before launch.
curl https://toome.cloud/api/v1/test-connection \
-H "Authorization: Bearer {your_token}"{ "status":"success", "app":{"name":"My App","tenant_id":42}, "scopes":{...} }GET /templates
Retrieve all Meta-approved message templates available on your account. A template message can only be sent once it appears in this list with an approved status.
{
"success": true,
"count": 2,
"data": [
{
"id": 5,
"name": "order_confirmed",
"language": "en",
"category": "UTILITY",
"body": "Hi 1, your order #2 has been confirmed ✅",
"variables_count": 2
}
]
}POST /notify
Send a WhatsApp template message to any phone number — even with no prior conversation with the user.
Use it for transactional notifications like order confirmations, appointment reminders, and shipping alerts.
Requires the notify scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | required | Phone number in international format without + (example: 201012345678) |
| template_name | string | required | Approved template name as it appears in GET /templates |
| variables | array | optional | Dynamic variable values in order, matching {{1}}, {{2}}, ... |
| language | string | optional | Language code (default: ar) |
curl -X POST https://toome.cloud/api/v1/notify \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "phone": "201012345678", "template_name": "order_confirmed", "variables": ["Ahmed", "#1042", "150 EGP"] }'
Http::withToken($token)->post('/api/v1/notify', [ 'phone' => '201012345678', 'template_name' => 'order_confirmed', 'variables' => ['Ahmed', '#1042', '150 EGP'], ]);
requests.post('/api/v1/notify', headers={'Authorization': f'Bearer {token}'}, json={ 'phone': '201012345678', 'template_name': 'order_confirmed', 'variables': ['Ahmed', '#1042', '150 EGP'] } )
{ "success": true, "message": "Notification sent successfully.", "phone": "201012345678", "template_used": "order_confirmed" }{ "error": "template_not_found", "message": "Template not found or not yet approved by Meta." }POST /notifications/order
Send a ready-made WhatsApp notification for a specific order status using Toome's Meta-approved templates.
No prior conversation with the customer is required — the message always gets through.
Consumes credits from your balance on every successful send.
Requires the send_notifications scope plus the Order Notifications feature enabled by the admin.
Order confirmation: 0.5 credit — status updates: 0.3 credit (values configurable by the admin).
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | required | Customer's phone number in international format without + (example: 201012345678) |
| status | string | required | Order status — see the available values below |
| tracking | string | required | The order number or tracking code in your system |
| customer_name | string | optional | Customer name shown in the message |
| store_name | string | optional | Store or platform name shown in the message |
| total | string | optional | Order total as text (example: "350 EGP") — shown in the confirmation message only |
Available status values
| Value | Meaning | Credits |
|---|---|---|
| order_confirmed | Order confirmed ✅ | 0.5 |
| assigned | Delivery agent assigned | 0.3 |
| out_for_delivery | Order out for delivery 🚗 | 0.3 |
| at_dropoff_point | Arrived at drop-off point | 0.3 |
| delivered | Delivered ✅ | 0.3 |
| failed_delivery | Delivery failed ❌ | 0.3 |
| rescheduled | Delivery rescheduled | 0.3 |
| returned | Returned | 0.3 |
curl -X POST https://toome.cloud/api/v1/notifications/order \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "phone": "201012345678", "status": "order_confirmed", "tracking": "ORD-1042", "customer_name": "Ahmed Ali", "store_name": "My Store", "total": "350 EGP" }'
Http::withToken($token)->post('https://toome.cloud/api/v1/notifications/order', [ 'phone' => $order->customer_phone, 'status' => 'order_confirmed', 'tracking' => $order->reference, 'customer_name' => $order->customer_name, 'store_name' => 'My Store', 'total' => $order->total . ' EGP', ]);
requests.post('https://toome.cloud/api/v1/notifications/order', headers={'Authorization': f'Bearer {token}'}, json={ 'phone': '201012345678', 'status': 'out_for_delivery', 'tracking': 'ORD-1042', 'customer_name': 'Ahmed Ali', } )
{
"success": true,
"message_id": "wamid.HBgM...",
"status_sent": "order_confirmed",
"credits_charged": 0.5,
"balance_after": 49.5
}{ "error": "INSUFFICIENT_CREDITS", "balance": 0.2, "required": 0.5 }{ "error": "FEATURE_DISABLED", "message": "Order notifications feature is not enabled for this application" }POST /orders
Receive an order from your external system and save it in Toome, with the option to notify the
customer via WhatsApp at the same time. Requires the orders.write scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| order_ref | string | required | Unique order number in your external system |
| customer_phone | string | required | Customer's phone number in international format |
| customer_name | string | optional | Customer's full name |
| total | number | optional | Total order value |
| currency | string | optional | ISO 4217 currency code (example: EGP, SAR, USD) |
| items | array | optional | List of products — each item: {name, qty, price} |
| status | string | optional | Order status: pending | confirmed | shipped | cancelled |
| notify_template | string | optional | Name of the notification template to send the customer as soon as the order is recorded |
| notify_variables | array | optional | Template variable values in the order they appear in the message body |
| meta | object | optional | Custom extra data stored alongside the order for later reference |
curl -X POST https://toome.cloud/api/v1/orders \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "order_ref": "ORD-1042", "customer_phone": "201012345678", "customer_name": "Ahmed Ali", "total": 350.00, "currency": "EGP", "items": [ {"name": "White T-Shirt", "qty": 2, "price": 175} ], "notify_template": "order_confirmed", "notify_variables": ["Ahmed", "ORD-1042"] }'
Http::withToken($token)->post('/api/v1/orders', [ 'order_ref' => 'ORD-1042', 'customer_phone' => '201012345678', 'total' => 350.00, 'notify_template' => 'order_confirmed', 'notify_variables' => ['Ahmed', 'ORD-1042'], ]);
{ "success": true, "order_id": 7, "order_ref": "ORD-1042", "notified": true }GET /orders
Retrieve the list of external orders stored in Toome, with pagination support.
Requires the orders.read scope.
POST /customers
Add a new customer to Toome's CRM, or update their data if a customer with the same phone number already exists.
Requires the customers.write scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | required | Phone number — used as the customer's unique identifier in the system |
| name | string | optional | Customer's full name |
| string | optional | Email address | |
| gender | string | optional | male | female | unknown |
| source | string | optional | Acquisition source (example: shopify, website, referral) |
| status | string | optional | Customer stage in the sales pipeline: lead | interested | customer |
curl -X POST https://toome.cloud/api/v1/customers \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "phone": "201012345678", "name": "Ahmed Ali", "email": "ahmed@example.com", "source": "shopify", "status": "customer" }'
{ "success": true, "action": "created", "customer": {"id":15,"name":"Ahmed Ali","phone":"201012345678"} }{ "success": true, "action": "updated", ... }GET /customers
Retrieve the customer list with text search, status filtering, and pagination support.
Requires the customers.read scope.
Overview
Instead of polling GET /customers, you can register a callback_url in your app's
settings (Webhook tab) and Toome will push events to it in real time as they happen —
including customers your AI agent captures on its own during a conversation (for example, a visitor who
chats with your floating widget and shares their name and phone number naturally — Toome creates/updates
a real customer record for them and fires this webhook automatically, with no extra code on your side).
Configure this from Developer Platform → your app → Webhook tab: set your
callback_url, tick the events you want to receive, and copy your webhook_secret
(auto-generated the first time you open that tab). A "Send test event" button is available there to
confirm your endpoint is reachable before going live.
Available Events
| Event | Fires when |
|---|---|
| customer.created | A new customer record is created for your tenant — via POST /customers, or automatically when the AI widget captures a new visitor's lead data during chat. |
| customer.updated | An existing customer's name, phone, email, address, or profile_data changes. |
| order.created | A new order or service order is created for your tenant — including one the AI widget opens automatically when a visitor confirms a purchase (products, with a shipping address) or a service interest (a pending inquiry, no address needed) that maps to a specific item in your catalog. |
The Webhook tab lets you tick other events too (message.received, order.updated, etc.) for future use —
today only customer.created, customer.updated, and order.created actually fire. Ticking the others has no effect yet.
Verifying the Signature
Every request carries an X-Toome-Signature header: sha256=<hmac>, an
HMAC-SHA256 of the raw JSON body using your webhook_secret. Verify it before trusting the
payload:
// In your webhook route handler $payload = $request->getContent(); $expected = 'sha256=' . hash_hmac('sha256', $payload, $webhookSecret); $signature = $request->header('X-Toome-Signature'); // constant-time compare, never === if (!hash_equals($expected, $signature)) { abort(401); } $event = $request->header('X-Toome-Event'); // e.g. "customer.created" $data = $request->json()->all();
Example payload for customer.created / customer.updated:
{ "event": "customer.created", "app_id": "tm_xxxxx", "timestamp": "2026-08-28T21:28:02+03:00", "data": { "id": 1913, "name": "محمد سالم", "phone": "+962791234567", "email": null, "platform_source": "widget", "profile_data": { "interest": "..." }, "created_at": "..." } }
If you configured customer_fields_out in the Scopes tab, only those fields (plus id)
are included in data. Delivery retries up to 3 times with backoff if your endpoint doesn't
return a 2xx response.
E-commerce store integration
When a new order arrives from a Shopify webhook, forward it to Toome to save it and instantly notify the customer via WhatsApp:
// Shopify webhook handler $order = $request->json()->all(); Http::withToken($token)->post('https://toome.cloud/api/v1/orders', [ 'order_ref' => $order['order_number'], 'customer_phone' => $order['phone'], 'customer_name' => $order['customer']['first_name'], 'total' => $order['total_price'], 'notify_template' => 'order_confirmed', 'notify_variables' => [$order['customer']['first_name'], $order['order_number']], ]);
Booking confirmations
Right after a booking is confirmed in your system, send the notification template directly to the customer without needing any prior conversation:
Http::withToken($token)->post('https://toome.cloud/api/v1/notify', [ 'phone' => $booking->customer_phone, 'template_name' => 'booking_confirmed', 'variables' => [ $booking->customer_name, $booking->service_name, $booking->date->format('Y-m-d H:i'), ], ]);
Automatic customer import
When a user completes registration on your site, add them to Toome's CRM automatically so the AI engine can start engaging with them right away:
// After a user completes registration on your site Http::withToken($token)->post('https://toome.cloud/api/v1/customers', [ 'phone' => $user->phone, 'name' => $user->name, 'email' => $user->email, 'source' => 'website_registration', 'status' => 'lead', ]);
🔐 One-Time Password (OTP) verification service
Toome Auth is a standalone service for sending verification codes (OTP) over WhatsApp — and soon SMS. It lets developers add a secure verification layer to any app or website in a few simple steps. The service supports two integration paths:
| Integration Path | Best For | Route Prefix | Auth Mechanism |
|---|---|---|---|
| Type A — TenantApp | A store or app owner with an active Toome subscription who has enabled the OTP feature | /api/v1/otp/* |
Bearer Token (via POST /token + otp scope) |
| Type B — Standalone OTP App | A standalone app dedicated purely to verification, with no Toome subscription needed | /api/auth/v1/otp/* |
Authorization: Bearer {api_key} + X-App-Secret |
This section covers Type B (the standalone app). If you're using Type A, see the
Authentication section and make sure the otp scope is enabled.
Toome Auth OTP Authentication
Every request on this path must include two HTTP headers: one carrying your application's API key, and the other its shared App Secret to verify your identity.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxx X-App-Secret: your_app_secret_here
You can find your API Key and App Secret in the Developer Portal →
OTP Apps → View Keys. The application must be in active status
(after being reviewed and approved by the admin team).
POST /api/auth/v1/otp/send
Send an OTP verification code to a phone number over WhatsApp. Returns a unique request_id used in the following steps.
Instead of immediately sending a paid template message, the response returns a
deep_link_url — a
wa.me link with a pre-filled message. Prompt the user to open it and tap "Send" inside WhatsApp,
and their reply reaches us as a normal free message, which we verify instantly via webhook (at no cost to you).
If the user doesn't reply within fallback_after_seconds seconds, we automatically send the regular
paid template message as a fallback — meaning verification is guaranteed either way, and cost only applies
when the template is actually needed. Track the status in real time with
GET /otp/status/{request_id}
instead of waiting for the user to type a code — on the free verification path, they won't type anything at all.
POST /api/auth/v1/otp/send
Authorization: Bearer sk_live_xxxx
X-App-Secret: your_secret
{
"phone": "201012345678",
"channel": "whatsapp",
"locale": "en",
"metadata": { "order_id": "ORD-789" }
}{
"success": true,
"request_id": "req_01JXXXXXXXXXXXXXXXXXXX",
"channel": "whatsapp",
"phone": "+201012345678",
"expires_in": 600,
"expires_at": "2026-06-12T14:05:00Z",
"verification_method": "whatsapp_deeplink",
"deep_link_url": "https://wa.me/9687xxxxxxx?text=TOOME-VERIFY%3Areq_01JXXX...",
"fallback_after_seconds": 25
}$res = Http::withHeaders([
'Authorization' => 'Bearer ' . $apiKey,
'X-App-Secret' => $appSecret,
])->post('https://toome.cloud/api/auth/v1/otp/send', [
'phone' => $user->phone,
'locale' => 'en',
]);
$requestId = $res->json('request_id');
$deepLink = $res->json('deep_link_url'); // show this as a "Verify via WhatsApp" button in your UI| Field | Type | Description |
|---|---|---|
phone * | string | Phone number in E.164 format without the + sign (example: 201012345678) |
channel | string | Delivery channel: whatsapp (default) | sms |
locale | string | Language of the fallback OTP template message: ar (default) | en |
metadata | object | Optional contextual data returned to you upon successful verification (example: order number, session id) |
The fields verification_method, deep_link_url, and fallback_after_seconds are only returned when channel = whatsapp.
The sms channel doesn't have a free path yet (only the base fields are returned).
POST /api/auth/v1/otp/verify
Verify the code entered by the user. On success, the code is invalidated immediately and can't be reused.
The request returns success: true along with any metadata
attached at send time (including verified_via set to template).
This route is only for the paid fallback case — if you're using free WhatsApp verification, watch
GET /otp/status/{request_id} instead.
{
"request_id": "req_01JXXXXXXXXXXXXXXXXXXX",
"code": "482917"
}// success
{
"success": true,
"phone": "201012345678",
"metadata": { "order_id": "ORD-789", "verified_via": "template" },
"verified_at": "2026-06-12T14:03:22Z"
}
// wrong code
{
"success": false,
"error_code": "INVALID_CODE",
"attempts_remaining": 2
}$res = Http::withHeaders([
'Authorization' => 'Bearer ' . $apiKey,
'X-App-Secret' => $appSecret,
])->post('https://toome.cloud/api/auth/v1/otp/verify', [
'request_id' => $requestId,
'code' => $userInput,
]);
if ($res->json('success')) {
// Verified — continue user registration or grant access
}POST /api/auth/v1/otp/resend
Resend an OTP code to the same number using the original request_id.
The cooldown period between sends (60 seconds by default) must elapse before this request is accepted.
{
"request_id": "req_01JXXXXXXXXXXXXXXXXXXX"
}
If this is called before the cooldown period ends, the server returns the error COOLDOWN_ACTIVE
along with a retry_after field indicating the remaining seconds.
GET /api/auth/v1/otp/status/{request_id}
Poll the current status of an OTP request — its primary use is the free WhatsApp verification path:
the user won't type a code in your UI, so there's nothing to call /otp/verify with.
Call this endpoint every 2-3 seconds after showing the user the deep_link_url, until
status becomes verified.
GET /api/auth/v1/otp/status/req_01JXXXXXXXXXXXXXXXXXXX Authorization: Bearer sk_live_xxxx X-App-Secret: your_secret
// still waiting for the user's reply on WhatsApp
{ "success": true, "found": true, "status": "pending", "verified_via": null }
// verified for free via a WhatsApp reply
{ "success": true, "found": true, "status": "verified", "verified_via": "webhook" }
// verified via the template code (after calling /otp/verify)
{ "success": true, "found": true, "status": "verified", "verified_via": "template" }
// expired or not found
{ "success": false, "found": false, "status": "not_found" }| Value | Meaning |
|---|---|
status: pending | Still waiting — either the WhatsApp reply or the fallback timeout |
status: verified | Verified — proceed with user registration immediately |
status: failed | The fallback message failed to send (rare) — try /otp/resend |
verified_via | webhook (free) or template (paid) — informational only, no difference in outcome |
DELETE /api/auth/v1/otp/{request_id}
Cancel an OTP request and invalidate its associated code immediately. Use it when the user verifies through an alternate method, backs out of the flow, or their session ends before verification completes.
DELETE /api/auth/v1/otp/req_01JXXXXXXXXXXXXXXXXXXX
GET /api/auth/v1/otp/logs
Retrieve a historical log of OTP requests, with filtering by phone number and status, plus pagination support. Useful for review and auditing purposes.
GET /api/auth/v1/otp/logs?status=verified&per_page=20&page=1
{
"data": [
{
"request_id": "req_01JXXX",
"phone": "201012345678",
"channel": "whatsapp",
"status": "verified",
"created_at": "2026-06-12T14:00:00Z"
}
],
"meta": { "total": 148, "per_page": 20, "current_page": 1 }
}
Available values for filtering by the status field:
pending (pending) |
verified (verified) |
expired (expired) |
cancelled (cancelled) |
failed (send failed)
GET /api/auth/v1/apps/stats
Retrieve OTP app performance statistics for the last 30 days, broken down by channel.
{
"total_sent": 1420,
"total_verified": 1184,
"verify_rate": "83.4%",
"today": 47,
"by_channel": {
"whatsapp": { "sent": 1400, "verified": 1170 },
"sms": { "sent": 20, "verified": 14 }
}
}Test Phones
This feature lets you register up to 10 phone numbers with fixed verification codes for testing purposes. When an OTP is sent to any of these numbers:
- No actual WhatsApp message is sent.
- No credits or tokens are consumed.
- The fixed code you pre-configured, stored in Redis, is returned.
- The feature works in both Live and Sandbox modes.
You can manage these numbers from the Developer Portal → OTP Apps → View App → Test Phones section.
The response includes a test_phone: true field for every request to a test number.
OTP Error Codes
The OTP API returns a specific text error_code to make handling different error cases easier programmatically.
| error_code | HTTP Code | Cause & Suggested Handling |
|---|---|---|
MISSING_CREDENTIALS | 401 | The Authorization or X-App-Secret header is missing from the request |
INVALID_API_KEY | 401 | The API key doesn't exist in the system — verify the key is correct |
INVALID_SECRET | 401 | App Secret doesn't match — check your application's credentials in the Developer Portal |
APP_PENDING_REVIEW | 403 | The application is still under review and hasn't been activated yet |
APP_REJECTED | 403 | The application request was rejected — contact support for details |
RATE_LIMITED | 429 | Exceeded the allowed rate (60 requests/minute per application) |
COOLDOWN_ACTIVE | 429 | The cooldown period between sends hasn't elapsed — check retry_after in the response |
INVALID_CODE | 422 | The entered code is incorrect — check attempts_remaining for how many tries are left |
OTP_EXPIRED | 410 | The OTP code has expired — send a new request via POST /otp/send |
OTP_ALREADY_VERIFIED | 409 | This code has already been verified — it can't be used again |
NOT_FOUND | 404 | The request_id doesn't exist or has expired in the database |