Toome AI Developer API Documentation
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.
New here? Follow this order
The sidebar is laid out as the same path most integrations follow, start to finish:
| Step | What you do |
|---|---|
| 1 | Connect — get an access token, then connect a WhatsApp number |
| 2 | WhatsApp Templates — create a template and get it approved by Meta |
| 3 | Send & Test — send your first message with that template |
| 4 | Categories — optional, but products and services can be organized under them |
| 5 | Products — bring in your catalog, if you sell physical products |
| 6 | Services — or your service catalog, if you take appointments instead (clinics, salons, consultants...) |
| 7 | Orders — push product orders in and notify customers automatically |
| 8 | Bookings — or book real appointments on a service's calendar |
| 9 | Customers — sync your customer/CRM data |
| 10 | Store Integration — connect an external store, if you have one |
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}.
expires_in is null). A token stops working immediately when the
application is disabled or no longer approved (403 APP_NOT_ACTIVE), and all of an application's tokens are revoked when its
secret is regenerated or the app is deleted (401) — request a new token from POST /token in that case.
If your secret is ever exposed, regenerate it from the Developer Portal.
Sandbox
Every application gets a second, separate credential pair — sandbox_client_id /
sandbox_client_secret — visible right below your live keys wherever they're shown (the
"App created" screen, and the app's Credentials tab). Use them exactly like the live
pair with POST /token; the response comes back with "sandbox": true.
What a sandbox token gets you:
- Same endpoints, same scopes. There's no separate
/sandbox/v1/...path — every endpoint documented on this page works with a sandbox token exactly like it does live. The environment is determined entirely by which credential pair minted the token. - A completely isolated fake account. A sandbox token reads and writes a private test tenant that only your app can see — its own customers, products, services, categories, orders, bookings, and WhatsApp templates. Nothing you create there is visible to, or shared with, your real account, and nothing in your real account leaks into it.
- A pre-connected fake WhatsApp number.
GET /whatsapp-accountsalready shows one — no connect flow needed (andPOST /whatsapp/connect-linkis disabled in sandbox, since connecting a real number to a fake account wouldn't make sense). - Zero real WhatsApp messages, ever.
POST /notify, order/booking notifications, and template submissions all respond exactly like the real thing (a fake but valid-lookingmessage_id, instant template approval that fires a genuinetemplate.approvedwebhook, and so on) — but nothing is ever sent to Meta, and no credits are ever charged. - Every response says so. A response from a sandbox-tokened request always includes
"sandbox": true, so it's never ambiguous which environment you're looking at.
Sandbox data is capped at 500 rows per resource type (customers, products, etc.) to keep things tidy —
past that, writes return 429 SANDBOX_LIMIT_REACHED. Clear it any time from the app's
Credentials tab with Reset Sandbox Data; this only wipes your test
data, not the sandbox account itself or its keys.
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 and list your connected WhatsApp numbers |
| whatsapp.manage | Generate connect links for your clients' WhatsApp numbers and toggle AI replies on connected numbers |
| 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 |
| products.write | Create, bulk-sync, and delete products in the catalog |
| products.read | Retrieve the product catalog with search and filtering support |
| brain.chat | Interact with the AI engine linked to your account |
notify scope also grants read access to the WhatsApp Accounts list. Changing AI replies or generating connect links needs the separate whatsapp.manage scope.
Endpoints under Your Account and Store Integration require no extra scope — any authenticated application can call them.
Error Codes
The API returns standard HTTP status codes along with a JSON body containing error details.
| HTTP Code | Meaning |
|---|---|
| 401 | Access token missing, invalid, or revoked (secret regenerated / app deleted) — issue a new token |
| 402 | INSUFFICIENT_CREDITS — account balance is not enough to complete the operation (OTP, WhatsApp, or notification credits). Top up from the Developer Portal Billing page; the response includes top_up_url where available |
| 403 | APP_NOT_ACTIVE — the application is disabled, or a live token was requested before admin approval (use your sandbox keys to test in the meantime). Otherwise the application doesn't have the required scope, the feature is disabled (FEATURE_DISABLED / OTP_FEATURE_DISABLED), or the endpoint isn't available in sandbox mode (SANDBOX_NOT_SUPPORTED, e.g. connecting a real WhatsApp number) |
| 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. In sandbox, also returned as SANDBOX_LIMIT_REACHED once a resource type hits its 500-row test cap |
| 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 /whatsapp-accounts
List the active WhatsApp numbers connected to your account. Requires the notify scope.
{
"success": true,
"count": 1,
"data": [
{ "id": 6, "phone_display": "+20 101 234 5678", "verified_name": "My Store", "is_primary": true, "status": "connected" }
]
}PATCH /whatsapp-accounts/{id}/ai
Turn Toome's automatic AI replies on or off for a specific WhatsApp number connected to your account.
Requires the whatsapp.manage scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| ai_enabled | boolean | required | true to enable automatic AI replies, false to disable |
{ "success": true, "message": "تم تفعيل الرد الذكي على هذا الرقم.", "ai_balance": 49.5 }{ "success": false, "error_code": "ACCOUNT_NOT_FOUND", "message": "الرقم المحدد غير موجود لهذا التننت." }POST /whatsapp/connect-link
Generate a one-time, 30-minute link that walks your end customer through connecting their own WhatsApp
Business number to Toome (Meta's Embedded Signup flow) — no Toome login required to open it.
Send this link to whoever needs to connect a number; each call issues a fresh token.
Requires the whatsapp.manage scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | optional | A note to help you identify this link later (example: the customer's name) |
curl -X POST https://toome.cloud/api/v1/whatsapp/connect-link \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "label": "Ahmed'"'"'s store" }'
{ "success": true, "connect_url": "https://developers.toome.cloud/connect-whatsapp/{token}", "expires_at": "2026-09-11T21:00:00+00:00" }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.
POST /templates below. Either way they go through Meta's review —
once approved, they appear here ready to use in POST /notify.
{
"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 /templates
Submit a new WhatsApp message template to Meta for review. This is the same review process as creating a
template from the Toome dashboard — it does not go live immediately. Poll
GET /templates/{id} for its approval status, and once it's
APPROVED you can use its name in POST /notify.
Requires the whatsapp.manage scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Lowercase letters, numbers and underscores only (example: order_shipped). Must be unique per language on your account. |
| category | string | required | UTILITY or MARKETING |
| language | string | required | Meta language code (example: ar, en, en_US) |
| body | string | required | Message text. Use {{1}}, {{2}}, ... for variables, in order. |
| header_text | string | optional | Plain text header shown above the body (max 60 characters). Media headers (image/video/document) aren't supported via the API yet — create those from the dashboard. |
| sender_account_id | integer | optional | Which connected WhatsApp number to submit this template to — the id field from GET /whatsapp-accounts. Defaults to your primary number. |
curl -X POST https://toome.cloud/api/v1/templates \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "name": "order_shipped", "category": "UTILITY", "language": "en", "body": "Hi 1, your order #2 is on its way!", "sender_account_id": 6 }'
{ "success": true, "id": 42, "name": "order_shipped", "status": "PENDING", "sender_account": { "id": 6, "phone_display": "+20 101 234 5678" }, "message": "Template submitted to Meta for review. Poll GET /templates/{id} for its approval status." }{ "success": false, "error_code": "TEMPLATE_NAME_TAKEN", "message": "A template with this name and language already exists for your account." }{ "success": false, "error_code": "SENDER_ACCOUNT_NOT_FOUND", "message": "..." }{ "success": false, "error_code": "WHATSAPP_NOT_CONNECTED", "message": "No active WhatsApp number is connected for this account yet." }{ "success": false, "error_code": "META_ERROR", "message": "...", "provider_error": { "code": 100, "message": "..." } }GET /templates/{id}
Check a single template's review status by its id (returned from POST /templates) — unlike
GET /templates, this returns it regardless of status, so you can poll it
right after submitting until it's no longer PENDING.
{ "success": true, "id": 42, "name": "order_shipped", "language": "en", "category": "UTILITY", "status": "APPROVED", "rejection_reason": null, "header_type": "none", "body": "Hi 1, your order #2 is on its way!", "variables_count": 2, "has_buttons": false }{ "success": true, "id": 43, "status": "REJECTED", "rejection_reason": "..." } — status is one of PENDING, APPROVED, REJECTED{ "success": false, "error_code": "TEMPLATE_NOT_FOUND", "message": "No template found with this id for your account." }DELETE /templates/{id}
Remove a template from your Toome account. Requires the whatsapp.manage scope.
{ "success": true, "message": "Template removed from your Toome account. It still exists on Meta — this only hides it from your Toome dashboard and this API; it is not deleted from WhatsApp Manager." }{ "success": false, "error_code": "TEMPLATE_NOT_FOUND", "message": "No template found with this id for your account." }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. Must match the template's approved language exactly as returned by GET /templates (e.g. ar, not ar_EG). Defaults to the template's own language. |
| sender_account_id | integer | optional | Send from a specific connected WhatsApp number instead of your primary one — the id field from GET /whatsapp-accounts. Useful if you have more than one number connected (e.g. one per branch/doctor). Cannot be combined with use_toome_number. |
| use_toome_number | boolean | optional | Send from Toome's own official number instead of yours — for accounts with no WhatsApp number connected yet. Deducts from your prepaid WhatsApp balance (see GET /tenant/credits). Cannot be combined with sender_account_id. |
/notify uses your primary number by default. Call GET /whatsapp-accounts first to get each number's id, then pass it as sender_account_id.
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", "message_id": "wamid.HBg..." }{ "success": false, "error_code": "TEMPLATE_NOT_FOUND", "message": "Template not found or not yet approved by Meta." }{ "success": false, "error_code": "VARIABLES_COUNT_MISMATCH", "expected_variables": 5, "provided_variables": 6 }{ "success": false, "error_code": "TEMPLATE_LANGUAGE_NOT_FOUND", "available_languages": ["ar"] }{ "success": false, "error_code": "TEMPLATE_PARAMS_MISMATCH", "provider_error": { "code": 132000, "message": "...", "fbtrace_id": "..." } }{ "success": false, "error_code": "CONFLICTING_SENDER_OPTIONS", "message": "Cannot set both sender_account_id and use_toome_number — pick one." }{ "success": false, "error_code": "SENDER_ACCOUNT_NOT_FOUND", "message": "The given sender_account_id doesn't exist or isn't active. Use GET /whatsapp-accounts to see the available numbers." }200 always means Meta accepted the message and returned a message_id. If WhatsApp rejects it, you get a non-2xx response with an
error_code and the original provider_error. Other codes you may see: TEMPLATE_PARAM_FORMAT_INVALID, TEMPLATE_PAUSED,
TEMPLATE_DISABLED, RECIPIENT_UNREACHABLE (all 422), WHATSAPP_NOT_CONNECTED (422) and SEND_FAILED (502).
Use variables_count and body from GET /templates to build the variables array.
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.
POST /notify with use_toome_number; see GET /tenant/credits).
The amount is reserved when the request arrives and refunded automatically if the message fails to send.
Top up from the Developer Portal Billing page.
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 ✅ | 15¢ |
| assigned | Delivery agent assigned | 15¢ |
| out_for_delivery | Order out for delivery 🚗 | 15¢ |
| at_dropoff_point | Arrived at drop-off point | 15¢ |
| delivered | Delivered ✅ | 15¢ |
| failed_delivery | Delivery failed ❌ | 15¢ |
| rescheduled | Delivery rescheduled | 15¢ |
| returned | Returned | 15¢ |
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",
"charged_cents": 15,
"balance_after_cents": 285
}{ "success": false, "error_code": "INSUFFICIENT_CREDITS", "required_cents": 15, "balance_cents": 5, "top_up_url": "..." }{ "success": false, "error_code": "FEATURE_DISABLED", "message": "Order notifications feature is not enabled for this application" }
Shared by both Products and Services — the same category can
hold either, or both, depending on type. Optional: category_id is nullable everywhere it's used.
GET /categories
List your categories. Filter with ?type=product or ?type=service to get only categories usable by that catalog (categories with type: "both" or no type always match). Requires the categories.read scope.
{
"success": true,
"data": [
{ "id": 3, "parent_id": null, "name": "العناية بالبشرة", "name_en": "Skin Care", "slug": "skin-care", "type": "both", "description": null, "is_active": true }
],
"meta": { "total": 1, "per_page": 50, "current_page": 1, "last_page": 1 }
}POST /categories
Create a category, optionally nested under an existing one via parent_id. Requires the categories.write scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | |
| type | string | required | product, service, or both |
| name_en / description / description_en | string | optional | |
| parent_id | integer | optional | Must be one of your own categories |
| is_active | boolean | optional | Defaults to true |
{ "success": true, "id": 3, "message": "تم إنشاء الفئة بنجاح." }PATCH /categories/{id}
Every field optional, same as POST /categories. Requires the categories.write scope.
{ "success": true, "message": "تم تحديث الفئة بنجاح." }{ "success": false, "errors": { "parent_id": ["The selected parent id is invalid."] } }DELETE /categories/{id}
Requires the categories.write scope.
{ "success": true, "message": "تم حذف الفئة." }{ "success": false, "error_code": "CATEGORY_NOT_EMPTY", "message": "This category still has items linked to it. Move or delete them first, then delete the category.", "products_count": 4, "services_count": 0, "subcategories_count": 1 }{ "success": false, "error_code": "CATEGORY_NOT_FOUND", "message": "No category found with this id for your account." }GET /products
Retrieve the product catalog with text search, category filtering, and pagination support.
Requires the products.read scope.
{
"success": true,
"data": [
{ "id": 12, "name": "White T-Shirt", "price": 175, "stock": 40, "sku": "TS-WHT", "is_active": true }
],
"meta": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 }
}POST /products
Create a single product, or update it if a product with the same sku already exists for your account.
Requires the products.write scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Product name |
| name_en | string | optional | English product name |
| description | string | optional | Product description |
| price | number | optional | Price (default: 0) |
| stock | integer | optional | Available quantity (default: 0) |
| sku | string | optional | Unique SKU — sending an existing sku updates that product instead of creating a new one |
| category_id | integer | optional | Category ID from your catalog |
| is_active | boolean | optional | Whether the product is visible/sellable (default: true) |
| image | string | optional | Public image URL |
curl -X POST https://toome.cloud/api/v1/products \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "name": "White T-Shirt", "price": 175, "stock": 40, "sku": "TS-WHT" }'
{ "success": true, "action": "created", "product_id": 12, "sku": "TS-WHT" }{ "success": true, "action": "updated", "product_id": 12, "sku": "TS-WHT" }POST /products/sync
Bulk upsert up to 500 products in a single call — ideal for a full catalog sync from an external store.
Matching is done by sku: existing SKUs are updated, new ones are created.
Requires the products.write scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| products | array | required | 1 to 500 product objects — each requires name and sku, and accepts the same optional fields as POST /products |
curl -X POST https://toome.cloud/api/v1/products/sync \ -H "Authorization: Bearer {token}" \ -H "Content-Type: application/json" \ -d '{ "products": [ {"name": "White T-Shirt", "sku": "TS-WHT", "price": 175, "stock": 40}, {"name": "Black T-Shirt", "sku": "TS-BLK", "price": 175, "stock": 25} ] }'
{ "success": true, "message": "تمت المزامنة بنجاح.", "created": 1, "updated": 1, "total": 2 }DELETE /products/{id}
Permanently delete a product from your catalog. Requires the products.write scope.
{ "success": true, "message": "تم حذف المنتج." }For appointment-based businesses (clinics, salons, real estate, consultants...) instead of physical products. Each service has its own working hours, appointment duration, and can require booking.
GET /services
List your service catalog. Requires the services.read scope.
{
"success": true,
"data": [
{ "id": 7, "name": "استشارة أولى", "name_en": "Initial Consultation", "price": 200, "has_price": true, "duration_minutes": 30, "requires_booking": true, "is_active": true, "category_id": null }
],
"meta": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 }
}has_price: false means the price varies per case (common for consultations) — price is null in that case, don't display a number to your users.
POST /services
Create a new service. Requires the services.write scope. Unlike products, there's no SKU-based upsert — use PATCH /services/{id} to update one you already created.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Service name |
| name_en / description / description_en | string | optional | |
| price | number | optional | Leave it out entirely for a "price varies" service (has_price becomes false) |
| duration_minutes | integer | optional | How long one appointment takes. Defaults to 60. |
| requires_booking | boolean | optional | Defaults to true |
| work_start / work_end | string | optional | 24h HH:MM format. Falls back to the account's default business hours if not set. |
| work_days | array | optional | Any of Sun,Mon,Tue,Wed,Thu,Fri,Sat. Defaults to Sun–Thu. |
| category_id | integer | optional | Must belong to your account |
| is_active | boolean | optional | Defaults to true |
{ "success": true, "id": 7, "message": "تم إنشاء الخدمة بنجاح." }PATCH /services/{id}
Update a service — every field is optional, only what you send gets changed. Requires the services.write scope. Same parameters as POST /services.
{ "success": true, "message": "تم تحديث الخدمة بنجاح." }{ "success": false, "error_code": "SERVICE_NOT_FOUND", "message": "No service found with this id for your account." }DELETE /services/{id}
Requires the services.write scope.
{ "success": true, "message": "تم حذف الخدمة." }GET /services/{id}/availability
Check open appointment slots for a service on a given date, before calling POST /bookings.
Requires the services.read scope.
{
"success": true,
"date": "2026-10-01",
"is_working_day": true,
"slots": [
{ "start": "09:00", "end": "09:30", "available": true },
{ "start": "09:30", "end": "10:00", "available": false },
{ "start": "10:00", "end": "10:30", "available": true }
]
}{ "success": true, "date": "2026-10-02", "is_working_day": false, "slots": [] }available: false once it's fully booked — not after the first booking.
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. Sending the same order_ref twice for your tenant is safe — see the idempotency note below. |
| 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 }
Idempotent by order_ref: if you call this endpoint again with an
order_ref you already sent for this tenant (for example after a network timeout retry),
no duplicate order is created and no second notification is sent — you get back
200 with the existing order and "idempotent": true instead of 201.
GET /orders
Retrieve the list of external orders stored in Toome, with pagination support.
Requires the orders.read scope.
Real appointments on the calendar for the services from Step 5 — check availability first, then book.
POST /bookings
Book a real, confirmed appointment for a customer. It appears on the account's calendar immediately and
the owner gets notified, same as a booking made through the dashboard. Requires the bookings.write scope.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| service_id | integer | required | From GET /services |
| customer_phone | string | required | International format without +. An existing customer with this phone is reused; otherwise a new one is created. |
| customer_name | string | optional | |
| appointment_at | string | required | YYYY-MM-DD HH:MM, in the account's own local time (not UTC) — match a start value from GET /services/{id}/availability |
| duration_minutes | integer | optional | Defaults to the service's own duration |
| notes | string | optional | |
| notify_template | string | optional | Approved template name (from GET /templates) to confirm the booking with the customer |
| notify_variables | array | optional |
{ "success": true, "id": 51, "service_order_id": 51, "order_code": "APP-7K2Q9X", "status": "confirmed", "appointment_at": "2026-10-01 09:00", "appointment_end_at": "2026-10-01 09:30", "notified": false }{ "success": false, "error_code": "SLOT_UNAVAILABLE", "message": "This time slot is fully booked. Check GET /services/{id}/availability for open slots." }{ "success": false, "error_code": "PAST_DATETIME", "message": "appointment_at must be in the future." }GET /bookings
List bookings, newest appointment first. Filterable by status, service_id, and date (YYYY-MM-DD). Requires the bookings.read scope.
{
"success": true,
"data": [
{ "id": 51, "order_code": "APP-7K2Q9X", "service_id": 7, "service_name": "استشارة أولى", "customer_name": "Ahmed Ali", "customer_phone": "201099998877", "status": "confirmed", "total_amount": 200, "appointment_at": "2026-10-01 09:00:00", "appointment_end_at": "2026-10-01 09:30:00" }
],
"meta": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 }
}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 |
| fields | object | optional | The account's custom customer fields as { "key": value } (see GET /customer-fields for the keys). Select fields take an option key or its label; multi-select takes an array; yes/no takes a boolean. The request is all-or-nothing: an unknown key, an invalid value, or a sensitive field (never writable through the API) rejects the whole request with 422 and nothing is saved. |
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 /customer-fields
List the account's custom customer fields (keys, labels, types and select options) so you know what to send in fields.
Sensitive fields are never listed and are never returned or accepted by the API. Requires the customers.read scope.
{ "success": true, "data": [ { "key": "budget", "label": "Budget", "type": "number", "options": null }, { "key": "skin_type", "label": "Skin type", "type": "select", "options": [ {"key":"dry","label":"Dry"} ] } ] }GET /customers
Retrieve the customer list with text search, status filtering, and pagination support.
Requires the customers.read scope.
Every customer includes a fields object with its non-sensitive custom field values (typed: numbers, booleans, arrays).
Add ?include=records to also get each customer's non-sensitive repeating records (visits-style entries, pets, etc.) as
records: [{ "id", "type", "date", "data": {…} }]. Sensitive fields and sensitive record types are never included.
Direct-database store integration — connects Toome to your store's own database so it can read/sync products directly.
New integrations should prefer the External Items API (pass site_type below) rather than exposing
direct database credentials.
POST /integrations/store/connect
Connect (or update) the external store linked to your application. Calling it again updates the existing connection instead of creating a duplicate.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| store_name | string | required | Your store's display name |
| store_url | string | optional | Your store's public URL — required when site_type is set |
| platform | string | optional | laravel | shopify | woocommerce | custom |
| site_type | string | optional | Set this to use the recommended External Items API mode instead of a direct DB connection — issues an items_api_token in the response |
| webhook_url | string | optional | Your endpoint for store-related webhook events |
| db_host / db_port / db_name / db_user / db_password | string | optional | Direct database credentials — only needed for the legacy direct-connection mode (not used with site_type). Must be a public, non-private host. |
{
"success": true,
"store_id": "st_...",
"app_id": "tm_...",
"tenant_id": 42,
"widget_key": "...",
"platform": "custom",
"items_api_token": "only present the first time, when site_type is used — store it now"
}{ "success": false, "error_code": "STORE_URL_REQUIRED", "message": "store_url is required when using site_type (External Items API mode)." }POST /integrations/store/test-connection
Test the direct database connection saved via /connect. Only relevant for the legacy direct-DB mode.
{ "success": true }{ "success": false, "error_code": "NO_STORE_CONNECTED", "message": "No store connected yet. Call /connect first." }{ "success": false, "error_code": "INVALID_DB_HOST", "message": "Database host is not allowed." }POST /integrations/store/sync
Not implemented yet. This endpoint currently returns 501 NOT_IMPLEMENTED. To push your catalog use
POST /products/sync, or connect an External Items API (site_type on /integrations/store/connect).
{ "success": false, "error_code": "NOT_IMPLEMENTED", "message": "Store sync is not implemented yet. ..." }{ "success": false, "error_code": "STORE_NOT_CONNECTED", "message": "Connect your store first." }DELETE /integrations/store/disconnect
Disconnect the store linked to your application. The connection record is kept but marked inactive.
{ "success": true, "message": "Store disconnected successfully." }GET /integrations/store/status
Check whether a store is connected and inspect its sync state.
{
"success": true,
"connected": true,
"store": {
"store_id": "st_...", "store_name": "My Store", "platform": "custom",
"site_type": "shopify", "widget_key": "...",
"items_api_connected": true, "last_sync_at": "2026-09-11T20:00:00+00:00", "synced_count": 4
}
}These endpoints require no additional scope — any authenticated application can call them.
GET /tenant/profile
Retrieve basic profile information for the tenant your app belongs to.
{ "success": true, "tenant": { "id": 42, "name": "My Store", "email": "owner@example.com", "phone": "+20 101 234 5678", "avatar_url": null, "created_at": "2026-01-10T09:00:00+00:00" } }GET /tenant/subscription
Retrieve the tenant's active plan, limits, and current usage.
{
"success": true,
"subscription": {
"plan_name": "Pro", "plan_code": "pro", "status": "active", "billing_cycle": "monthly",
"started_at": "2026-01-10T09:00:00+00:00", "expires_at": "2026-10-10T09:00:00+00:00", "is_trial": false,
"limits": { "conversations_per_month": 3000, "agents": 3, "apps": 2 },
"usage": { "conversations_this_month": 812, "agents_used": 2, "apps_used": 1 }
}
}{ "success": true, "subscription": null } — no active subscriptionGET /tenant/credits
Retrieve your developer credits (OTP / AI / WhatsApp balances) plus the tenant wallet balance and its 10 most recent transactions.
whatsapp_balance_cents is in cents (100 = 1 USD); each WhatsApp message sent from Toome's number costs 15 cents.
{
"success": true,
"developer_credits": {
"otp_balance": 50, "ai_balance": 100, "whatsapp_balance_cents": 300,
"otp_total_used": 0, "ai_total_used": 0, "whatsapp_total_used_cents": 0
},
"credits": {
"balance": 49.5, "currency": "EGP", "last_topped_up_at": "2026-09-01T12:00:00+00:00",
"recent_transactions": [
{ "type": "deduct", "amount": -0.5, "description": "notify: order_confirmed", "created_at": "2026-09-11T19:00:00+00:00" }
]
}
}GET /tenant/summary
A single call combining /tenant/profile, /tenant/subscription, /tenant/credits, and your app's own details — handy for a dashboard home screen.
{ "success": true, "tenant": {...}, "subscription": {...}, "credits": {...}, "app": {...} }GET /apps/{app_id}
Retrieve your own application's details. app_id must match the client_id of the token used to authenticate — you can only read your own app.
{
"success": true,
"app": {
"id": "tm_xxxxxxxxxxxxxxxx", "name": "My App", "description": "...", "status": "approved",
"is_active": true, "scopes": ["notify","orders.write"], "webhook_url": "https://...",
"created_at": "2026-01-10T09:00:00+00:00", "features": {...}
}
}{ "success": false, "error_code": "FORBIDDEN", "message": "You can only access your own app." }PATCH /apps/{app_id}/features
Toggle optional feature flags on your own application. Enabling a feature that requires a higher plan
than the tenant currently has is rejected per-feature (reported in blocked) rather than failing the whole request.
Some features (currently order_notifications) can only be disabled from the API — enabling them requires Toome admin approval and is always reported in blocked.
The otp feature is enabled by default on new apps and is billed per message from your OTP credits.
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| features | object | required | Map of feature key → boolean. Unknown feature keys are silently ignored. |
{ "success": true, "updated": { "some_feature": true }, "blocked": { "other_feature": "Requires pro plan or higher" } }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. |
| order.updated | Any field on an existing service order changes (status, notes, etc.). |
| order.paid | A service order's status changes to completed — the same point revenue is recorded for it. |
| whatsapp_account.connected | A WhatsApp number finishes connecting to your tenant — including one connected by your end customer through a connect-link you generated. Fires once per new number, not on token refreshes of an already-connected one. |
| subscription.expiring | Your tenant's Toome subscription is a few days away from expiring. |
| subscription.expired | Your tenant's Toome subscription has expired. |
| template.approved | A WhatsApp template you submitted via POST /templates (or the dashboard) is approved by Meta. Detected the moment it's discovered — either by polling GET /templates/{id} or an owner clicking "Sync with Meta" on the dashboard. |
| template.rejected | Same as above, but Meta rejected the template. The payload includes rejection_reason. |
| booking.confirmed | A booking (service order) becomes confirmed — whether it was created that way directly via POST /bookings, or transitioned into it from pending. |
| booking.cancelled | A booking's status changes to cancelled. |
| booking.completed | A booking's status changes to completed (the appointment took place). |
| booking.no_show | A booking's status changes to no_show (the customer didn't show up). |
All events listed above are live today. Tick the ones you need in Developer Platform → your app → Webhook tab.
Example payload for the booking events (booking.confirmed, booking.cancelled, booking.completed, booking.no_show):
{ "event": "booking.confirmed", "app_id": "tm_xxxxx", "timestamp": "2026-09-23T21:28:02+03:00", "data": { "id": 51, "order_code": "APP-7K2Q9X", "service_id": 7, "service_name": "استشارة عامة", "customer_name": "Ahmed Ali", "customer_phone": "201012345678", "status": "confirmed", "appointment_at": "2026-10-01 09:00:00" } }Example payload for the template events (template.approved, template.rejected):
{ "event": "template.rejected", "app_id": "tm_xxxxx", "timestamp": "2026-09-23T21:28:02+03:00", "data": { "id": 43, "name": "order_shipped", "language": "en", "category": "UTILITY", "status": "REJECTED", "rejection_reason": "..." } }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. data also carries a fields object with the customer's non-sensitive custom fields (opt in with the "Custom customer fields" checkbox); sensitive fields are never sent. Delivery retries up to 3 times with backoff if your endpoint doesn't
return a 2xx response.
🔐 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 |
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', ]);
Need help?
Integration questions, API keys, app approval status or webhook issues — email
developers@toome.cloud
and include your app_id so we can find your application quickly.
| Topic | Write to |
|---|---|
| API, keys, approvals, webhooks, sandbox | developers@toome.cloud |
| Billing, credits and invoices for the developer plan | billing@toome.cloud |
| Partnerships, agencies and the certified-partners directory | partners@toome.cloud |
| Privacy and data requests | privacy@toome.cloud |