Toome AI Developer API Documentation

API Reference
API Reference

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.

Base URL: https://toome.cloud/api/v1
All 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:

StepWhat you do
1Connect — get an access token, then connect a WhatsApp number
2WhatsApp Templates — create a template and get it approved by Meta
3Send & Test — send your first message with that template
4Categories — optional, but products and services can be organized under them
5Products — bring in your catalog, if you sell physical products
6Services — or your service catalog, if you take appointments instead (clinics, salons, consultants...)
7Orders — push product orders in and notify customers automatically
8Bookings — or book real appointments on a service's calendar
9Customers — sync your customer/CRM data
10Store Integration — connect an external store, if you have one
Prefer testing in Postman? Import the full Toome collection — every endpoint, organized into folders, ready to run.
Run in Postman or copy the collection URL

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:

  1. Register a new application from the Developer Portal to get your client_id and client_secret.
  2. Send a POST /api/v1/token request with these credentials to obtain an access_token.
  3. Attach the access token to every subsequent request in the header: Authorization: Bearer {token}.
Access tokens do not expire automatically (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.

Sandbox tokens work immediately — even while your app is still pending admin review. This is the main reason to reach for them: you don't have to wait for approval to start integrating.

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-accounts already shows one — no connect flow needed (and POST /whatsapp/connect-link is 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-looking message_id, instant template approval that fires a genuine template.approved webhook, 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.

ScopeGrants
notifySend WhatsApp notifications via Meta-approved message templates and list your connected WhatsApp numbers
whatsapp.manageGenerate connect links for your clients' WhatsApp numbers and toggle AI replies on connected numbers
send_notificationsSend WhatsApp order-status notifications (confirmed, shipped, delivered...) via Toome's built-in templates — consumes credits per message
orders.writeCreate external orders and update their status in Toome
orders.readRetrieve and view stored orders
customers.writeAdd new customers and edit their data in the CRM
customers.readRetrieve customer data with search and filtering support
products.writeCreate, bulk-sync, and delete products in the catalog
products.readRetrieve the product catalog with search and filtering support
brain.chatInteract with the AI engine linked to your account
The 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 CodeMeaning
401Access token missing, invalid, or revoked (secret regenerated / app deleted) — issue a new token
402INSUFFICIENT_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
403APP_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)
404The requested resource doesn't exist (e.g. an undefined message template)
422Validation error on the submitted data — check the errors field in the response
429You'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
502Message delivery failed via the Meta API — may be a temporary WhatsApp-side issue
Step 1 · Connect

POST /token

Issue an access token using the application's credentials. This token is used in the Authorization header for all subsequent API requests.

POST /api/v1/token

Request Body

FieldTypeRequiredDescription
client_idstringrequiredYour application's unique identifier — starts with tm_
client_secretstringrequiredYour 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']
200 { "access_token": "...", "token_type": "Bearer", "scopes": ["notify","orders.write"] }
401 { "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.

GET/api/v1/test-connection
curl https://toome.cloud/api/v1/test-connection \
  -H "Authorization: Bearer {your_token}"
200 { "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.

GET/api/v1/whatsapp-accounts
200
{
  "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.

The first time AI is enabled on a number, a one-time AI credit bonus is granted automatically — no action needed on your side.
PATCH/api/v1/whatsapp-accounts/6/ai

Request Parameters

FieldTypeRequiredDescription
ai_enabledbooleanrequiredtrue to enable automatic AI replies, false to disable
200 { "success": true, "message": "تم تفعيل الرد الذكي على هذا الرقم.", "ai_balance": 49.5 }
404 { "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.

POST/api/v1/whatsapp/connect-link

Request Parameters

FieldTypeRequiredDescription
labelstringoptionalA 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" }'
200 { "success": true, "connect_url": "https://developers.toome.cloud/connect-whatsapp/{token}", "expires_at": "2026-09-11T21:00:00+00:00" }
Step 2 · WhatsApp Templates

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.

GET/api/v1/templates
Templates can be created from inside the Toome dashboard, or submitted directly with POST /templates below. Either way they go through Meta's review — once approved, they appear here ready to use in POST /notify.
200
{
  "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.

POST/api/v1/templates

Request Parameters

FieldTypeRequiredDescription
namestringrequiredLowercase letters, numbers and underscores only (example: order_shipped). Must be unique per language on your account.
categorystringrequiredUTILITY or MARKETING
languagestringrequiredMeta language code (example: ar, en, en_US)
bodystringrequiredMessage text. Use {{1}}, {{2}}, ... for variables, in order.
header_textstringoptionalPlain 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_idintegeroptionalWhich 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
  }'
201 { "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." }
422 { "success": false, "error_code": "TEMPLATE_NAME_TAKEN", "message": "A template with this name and language already exists for your account." }
404 { "success": false, "error_code": "SENDER_ACCOUNT_NOT_FOUND", "message": "..." }
422 { "success": false, "error_code": "WHATSAPP_NOT_CONNECTED", "message": "No active WhatsApp number is connected for this account yet." }
422 { "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.

GET/api/v1/templates/42
Every call checks Meta directly for this template's current status (not just a cached copy) and updates it on our side too — no separate "refresh" step needed, just call this endpoint again.
200 { "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 }
200 { "success": true, "id": 43, "status": "REJECTED", "rejection_reason": "..." } — status is one of PENDING, APPROVED, REJECTED
404 { "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.

This does not delete anything on Meta's side. It only hides the template from your Toome dashboard and this API — the template keeps existing (and stays usable) on your WhatsApp Business Account. To actually remove it from Meta, do that from WhatsApp Manager directly.
DELETE/api/v1/templates/42
200 { "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." }
404 { "success": false, "error_code": "TEMPLATE_NOT_FOUND", "message": "No template found with this id for your account." }
Step 3 · Send & Test

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.

POST/api/v1/notify

Request Parameters

FieldTypeRequiredDescription
phonestringrequiredPhone number in international format without + (example: 201012345678)
template_namestringrequiredApproved template name as it appears in GET /templates
variablesarrayoptionalDynamic variable values in order, matching {{1}}, {{2}}, ...
languagestringoptionalLanguage 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_idintegeroptionalSend 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_numberbooleanoptionalSend 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.
Sending from a specific number: if your account has more than one WhatsApp number connected, /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']
    }
)
200 { "success": true, "message": "Notification sent successfully.", "phone": "201012345678", "template_used": "order_confirmed", "message_id": "wamid.HBg..." }
404 { "success": false, "error_code": "TEMPLATE_NOT_FOUND", "message": "Template not found or not yet approved by Meta." }
422 { "success": false, "error_code": "VARIABLES_COUNT_MISMATCH", "expected_variables": 5, "provided_variables": 6 }
422 { "success": false, "error_code": "TEMPLATE_LANGUAGE_NOT_FOUND", "available_languages": ["ar"] }
422 { "success": false, "error_code": "TEMPLATE_PARAMS_MISMATCH", "provider_error": { "code": 132000, "message": "...", "fbtrace_id": "..." } }
422 { "success": false, "error_code": "CONFLICTING_SENDER_OPTIONS", "message": "Cannot set both sender_account_id and use_toome_number — pick one." }
404 { "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." }
A 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/api/v1/notifications/order
Credit consumption: every message sent from Toome's number costs 15 cents from your WhatsApp balance (the same balance used by 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

FieldTypeRequiredDescription
phonestringrequiredCustomer's phone number in international format without + (example: 201012345678)
statusstringrequiredOrder status — see the available values below
trackingstringrequiredThe order number or tracking code in your system
customer_namestringoptionalCustomer name shown in the message
store_namestringoptionalStore or platform name shown in the message
totalstringoptionalOrder total as text (example: "350 EGP") — shown in the confirmation message only

Available status values

ValueMeaningCredits
order_confirmedOrder confirmed ✅15¢
assignedDelivery agent assigned15¢
out_for_deliveryOrder out for delivery 🚗15¢
at_dropoff_pointArrived at drop-off point15¢
deliveredDelivered ✅15¢
failed_deliveryDelivery failed ❌15¢
rescheduledDelivery rescheduled15¢
returnedReturned15¢
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',
    }
)
200
{
  "success": true,
  "message_id": "wamid.HBgM...",
  "status_sent": "order_confirmed",
  "charged_cents": 15,
  "balance_after_cents": 285
}
402 { "success": false, "error_code": "INSUFFICIENT_CREDITS", "required_cents": 15, "balance_cents": 5, "top_up_url": "..." }
403 { "success": false, "error_code": "FEATURE_DISABLED", "message": "Order notifications feature is not enabled for this application" }
Step 4 · Categories

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.

GET/api/v1/categories?type=service
200
{
  "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.

POST/api/v1/categories

Request Parameters

FieldTypeRequiredDescription
namestringrequired 
typestringrequiredproduct, service, or both
name_en / description / description_enstringoptional 
parent_idintegeroptionalMust be one of your own categories
is_activebooleanoptionalDefaults to true
201 { "success": true, "id": 3, "message": "تم إنشاء الفئة بنجاح." }

PATCH /categories/{id}

Every field optional, same as POST /categories. Requires the categories.write scope.

PATCH/api/v1/categories/3
200 { "success": true, "message": "تم تحديث الفئة بنجاح." }
422 { "success": false, "errors": { "parent_id": ["The selected parent id is invalid."] } }

DELETE /categories/{id}

Requires the categories.write scope.

DELETE/api/v1/categories/3
Refuses to delete a category that still has products, services, or subcategories linked to it — move or delete those first. This is stricter than the dashboard (which deletes everything in it after a confirmation prompt a human sees) since an API call has no such confirmation step.
200 { "success": true, "message": "تم حذف الفئة." }
422 { "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 }
404 { "success": false, "error_code": "CATEGORY_NOT_FOUND", "message": "No category found with this id for your account." }
Step 5 · Products

GET /products

Retrieve the product catalog with text search, category filtering, and pagination support. Requires the products.read scope.

GET/api/v1/products?search=shirt&category_id=3&active=1&per_page=20
200
{
  "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.

POST/api/v1/products

Request Parameters

FieldTypeRequiredDescription
namestringrequiredProduct name
name_enstringoptionalEnglish product name
descriptionstringoptionalProduct description
pricenumberoptionalPrice (default: 0)
stockintegeroptionalAvailable quantity (default: 0)
skustringoptionalUnique SKU — sending an existing sku updates that product instead of creating a new one
category_idintegeroptionalCategory ID from your catalog
is_activebooleanoptionalWhether the product is visible/sellable (default: true)
imagestringoptionalPublic 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"
  }'
201 { "success": true, "action": "created", "product_id": 12, "sku": "TS-WHT" }
200 { "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.

POST/api/v1/products/sync

Request Parameters

FieldTypeRequiredDescription
productsarrayrequired1 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}
    ]
  }'
200 { "success": true, "message": "تمت المزامنة بنجاح.", "created": 1, "updated": 1, "total": 2 }

DELETE /products/{id}

Permanently delete a product from your catalog. Requires the products.write scope.

DELETE/api/v1/products/12
200 { "success": true, "message": "تم حذف المنتج." }
Step 6 · Services

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.

GET/api/v1/services?per_page=20
200
{
  "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.

POST/api/v1/services

Request Parameters

FieldTypeRequiredDescription
namestringrequiredService name
name_en / description / description_enstringoptional 
pricenumberoptionalLeave it out entirely for a "price varies" service (has_price becomes false)
duration_minutesintegeroptionalHow long one appointment takes. Defaults to 60.
requires_bookingbooleanoptionalDefaults to true
work_start / work_endstringoptional24h HH:MM format. Falls back to the account's default business hours if not set.
work_daysarrayoptionalAny of Sun,Mon,Tue,Wed,Thu,Fri,Sat. Defaults to Sun–Thu.
category_idintegeroptionalMust belong to your account
is_activebooleanoptionalDefaults to true
201 { "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.

PATCH/api/v1/services/7
200 { "success": true, "message": "تم تحديث الخدمة بنجاح." }
404 { "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.

DELETE/api/v1/services/7
200 { "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.

GET/api/v1/services/7/availability?date=2026-10-01
200
{
  "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 }
  ]
}
200 { "success": true, "date": "2026-10-02", "is_working_day": false, "slots": [] }
If the account allows multiple customers per slot (group bookings), a slot only becomes available: false once it's fully booked — not after the first booking.
Step 7 · Orders

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.

POST/api/v1/orders

Request Parameters

FieldTypeRequiredDescription
order_refstringrequiredUnique order number in your external system. Sending the same order_ref twice for your tenant is safe — see the idempotency note below.
customer_phonestringrequiredCustomer's phone number in international format
customer_namestringoptionalCustomer's full name
totalnumberoptionalTotal order value
currencystringoptionalISO 4217 currency code (example: EGP, SAR, USD)
itemsarrayoptionalList of products — each item: {name, qty, price}
statusstringoptionalOrder status: pending | confirmed | shipped | cancelled
notify_templatestringoptionalName of the notification template to send the customer as soon as the order is recorded
notify_variablesarrayoptionalTemplate variable values in the order they appear in the message body
metaobjectoptionalCustom 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'],
]);
201 { "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.

GET/api/v1/orders?per_page=20
Step 8 · Bookings

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.

POST/api/v1/bookings

Request Parameters

FieldTypeRequiredDescription
service_idintegerrequiredFrom GET /services
customer_phonestringrequiredInternational format without +. An existing customer with this phone is reused; otherwise a new one is created.
customer_namestringoptional 
appointment_atstringrequiredYYYY-MM-DD HH:MM, in the account's own local time (not UTC) — match a start value from GET /services/{id}/availability
duration_minutesintegeroptionalDefaults to the service's own duration
notesstringoptional 
notify_templatestringoptionalApproved template name (from GET /templates) to confirm the booking with the customer
notify_variablesarrayoptional 
201 { "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 }
409 { "success": false, "error_code": "SLOT_UNAVAILABLE", "message": "This time slot is fully booked. Check GET /services/{id}/availability for open slots." }
422 { "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.

GET/api/v1/bookings?status=confirmed&per_page=20
200
{
  "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 }
}
Step 9 · Customers

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.

POST/api/v1/customers

Request Parameters

FieldTypeRequiredDescription
phonestringrequiredPhone number — used as the customer's unique identifier in the system
namestringoptionalCustomer's full name
emailstringoptionalEmail address
genderstringoptionalmale | female | unknown
sourcestringoptionalAcquisition source (example: shopify, website, referral)
statusstringoptionalCustomer stage in the sales pipeline: lead | interested | customer
fieldsobjectoptionalThe 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"
  }'
201 { "success": true, "action": "created", "customer": {"id":15,"name":"Ahmed Ali","phone":"201012345678"} }
200 { "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.

GET/api/v1/customer-fields
200 { "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.

GET/api/v1/customers?status=customer&search=ahmed&per_page=20

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.

Step 10 · Store Integration

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.

POST/api/v1/integrations/store/connect

Request Parameters

FieldTypeRequiredDescription
store_namestringrequiredYour store's display name
store_urlstringoptionalYour store's public URL — required when site_type is set
platformstringoptionallaravel | shopify | woocommerce | custom
site_typestringoptionalSet this to use the recommended External Items API mode instead of a direct DB connection — issues an items_api_token in the response
webhook_urlstringoptionalYour endpoint for store-related webhook events
db_host / db_port / db_name / db_user / db_passwordstringoptionalDirect database credentials — only needed for the legacy direct-connection mode (not used with site_type). Must be a public, non-private host.
201
{
  "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"
}
422 { "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.

POST/api/v1/integrations/store/test-connection
200 { "success": true }
404 { "success": false, "error_code": "NO_STORE_CONNECTED", "message": "No store connected yet. Call /connect first." }
422 { "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).

POST/api/v1/integrations/store/sync
501 { "success": false, "error_code": "NOT_IMPLEMENTED", "message": "Store sync is not implemented yet. ..." }
404 { "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.

DELETE/api/v1/integrations/store/disconnect
200 { "success": true, "message": "Store disconnected successfully." }

GET /integrations/store/status

Check whether a store is connected and inspect its sync state.

GET/api/v1/integrations/store/status
200
{
  "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
  }
}
Your Account

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.

GET/api/v1/tenant/profile
200 { "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.

GET/api/v1/tenant/subscription
200
{
  "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 }
  }
}
200 { "success": true, "subscription": null } — no active subscription

GET /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.

GET/api/v1/tenant/credits
200
{
  "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.

GET/api/v1/tenant/summary
200 { "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.

GET/api/v1/apps/tm_xxxxxxxxxxxxxxxx
200
{
  "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": {...}
  }
}
403 { "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.

PATCH/api/v1/apps/tm_xxxxxxxxxxxxxxxx/features

Request Parameters

FieldTypeRequiredDescription
featuresobjectrequiredMap of feature key → boolean. Unknown feature keys are silently ignored.
200 { "success": true, "updated": { "some_feature": true }, "blocked": { "other_feature": "Requires pro plan or higher" } }
Outbound Webhooks

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

EventFires when
customer.createdA 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.updatedAn existing customer's name, phone, email, address, or profile_data changes.
order.createdA 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.updatedAny field on an existing service order changes (status, notes, etc.).
order.paidA service order's status changes to completed — the same point revenue is recorded for it.
whatsapp_account.connectedA 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.expiringYour tenant's Toome subscription is a few days away from expiring.
subscription.expiredYour tenant's Toome subscription has expired.
template.approvedA 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.rejectedSame as above, but Meta rejected the template. The payload includes rejection_reason.
booking.confirmedA booking (service order) becomes confirmed — whether it was created that way directly via POST /bookings, or transitioned into it from pending.
booking.cancelledA booking's status changes to cancelled.
booking.completedA booking's status changes to completed (the appointment took place).
booking.no_showA 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.

Toome Auth

🔐 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).

Key security: Never embed the API Key or App Secret in frontend code (JavaScript/Mobile). All OTP requests must be made exclusively from your backend server.

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.

Free WhatsApp verification (default and mandatory for the WhatsApp channel)
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
FieldTypeDescription
phone *stringPhone number in E.164 format without the + sign (example: 201012345678)
channelstringDelivery channel: whatsapp (default) | sms
localestringLanguage of the fallback OTP template message: ar (default) | en
metadataobjectOptional 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" }
ValueMeaning
status: pendingStill waiting — either the WhatsApp reply or the fallback timeout
status: verifiedVerified — proceed with user registration immediately
status: failedThe fallback message failed to send (rare) — try /otp/resend
verified_viawebhook (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.

Test phones are meant for development environments only. When you go live, no code change is needed — just remove the numbers from the list, or stop sending to them.

OTP Error Codes

The OTP API returns a specific text error_code to make handling different error cases easier programmatically.

error_codeHTTP CodeCause & Suggested Handling
MISSING_CREDENTIALS401The Authorization or X-App-Secret header is missing from the request
INVALID_API_KEY401The API key doesn't exist in the system — verify the key is correct
INVALID_SECRET401App Secret doesn't match — check your application's credentials in the Developer Portal
APP_PENDING_REVIEW403The application is still under review and hasn't been activated yet
APP_REJECTED403The application request was rejected — contact support for details
RATE_LIMITED429Exceeded the allowed rate (60 requests/minute per application)
COOLDOWN_ACTIVE429The cooldown period between sends hasn't elapsed — check retry_after in the response
INVALID_CODE422The entered code is incorrect — check attempts_remaining for how many tries are left
OTP_EXPIRED410The OTP code has expired — send a new request via POST /otp/send
OTP_ALREADY_VERIFIED409This code has already been verified — it can't be used again
NOT_FOUND404The request_id doesn't exist or has expired in the database
Use Cases

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',
]);
Support & contact

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.

TopicWrite to
API, keys, approvals, webhooks, sandboxdevelopers@toome.cloud
Billing, credits and invoices for the developer planbilling@toome.cloud
Partnerships, agencies and the certified-partners directorypartners@toome.cloud
Privacy and data requestsprivacy@toome.cloud