Early access preview. The Jwab Business API is not generally available yet; endpoints and details may change. To join early access, contact us. Back to jwab.ai
Send WhatsApp invoices, quotations and updates from your own systems
Connect your ERP, CRM, accounting or e‑commerce platform to the WhatsApp number you already use in Jwab. Send approved templates and documents, get delivery and read receipts, and receive customer replies in your system.
Design preview. This page shows how the API will work when it launches. Endpoint names and fields may still change a little before general availability; every change will be listed in a changelog.
InvoicesSend the PDF with a “Pay now” link the moment the invoice is posted.
QuotationsAttach the quote; the customer taps Accept or Request changes and your CRM updates.
Reminders and updatesPayment due, order shipped, appointment confirmed, account statements.
Shared inboxReplies land in Jwab, with the right department, so your team can take over.
Quick start
Connect your WhatsApp number in Jwab and get a utility template approved, for example invoice_ready with a document header and four variables.
Create an API key in Jwab → Settings → Developers → API keys. Start with a test key (jwab_test_…): it checks every request like a live key but sends nothing.
content is the PDF file encoded as base64; most languages do this in one line (base64_encode(file_get_contents($path)) in PHP, base64.b64encode(pdf_bytes).decode() in Python). If the PDF is already online, send url instead; see Attaching a PDF.
You get 202 Accepted with a message ID. When the customer receives and reads it, Jwab calls your webhook with message.delivered and message.read, including your client_reference.
Authentication
Send your API key as a bearer token on every request:
Authorization: Bearer jwab_live_8f3c…
Workspace admins create keys in Settings → Developers → API keys. A key is shown once; you can rotate or revoke it at any time.
jwab_live_… keys send real messages. jwab_test_… keys return realistic responses and webhooks without sending anything or using your quota.
Limit each key to the scopes it needs, and optionally to a list of server IP addresses.
Call the API only from your server. Never put a key in a website or mobile app.
Scope
Allows
messages:send
Send messages and batches
messages:read
Read message status and history
templates:read
List and read templates
templates:write
Create and delete templates
contacts:write
Create, update and opt out contacts
media:write
Upload and delete media
webhooks:manage
Manage webhook endpoints
Conventions
Base URLhttps://api.jwab.ai/v1
JSON in and out, UTF‑8. Arabic text is fully supported in templates, names and file names.
Phone numbers in E.164, e.g. +201001234567. We also accept 00201001234567 and 201001234567 and normalize them.
IDs carry a prefix that tells you what they are: msg_, ch_, tpl_, ct_, med_, wh_, bat_.
Times are ISO‑8601 in UTC.
Idempotency. Send an Idempotency-Key header on every POST, for example your invoice number. If you retry with the same key within 24 hours you get the first result back, and the customer never gets the message twice.
Your own references. Put your invoice number or CRM deal ID in client_reference and anything else in metadata (up to 20 keys). Both come back on every response and webhook, so you don't need to store our IDs.
Attaching a PDF. Put the file in document one of three ways: content, the file itself as base64 (up to 15 MB, one request, nothing to host); url, a link Jwab can download; or media_id from Upload media, for larger files or one file sent to many customers. Always include filename.
Pagination. List endpoints take limit (1–100, default 25) and cursor, and return has_more and next_cursor.
WhatsApp sending rules
WhatsApp decides what you may send based on when the customer last wrote to you.
ALWAYS Approved templates
Start a conversation or message anyone at any time with a template Meta has approved. Invoices, quotations and reminders go this way.
24 H Free‑form messages
Text, documents, images and buttons are allowed for 24 hours after the customer's last message. Outside that window you get 409 outside_service_window.
Templates are created in Jwab or with Create template. Meta usually approves within minutes to a few hours.
Customers who opted out of marketing don't receive marketing templates (409 contact_opted_out). Utility and authentication templates still reach them.
Each contact's service_window_open_until tells you whether a free‑form message is allowed right now.
Message status
Sending is asynchronous. 202 Accepted means Jwab has queued the message. Each status change is sent to your webhook and is also available from Get message.
queued→sent→delivered→readorfailed
Plans, quotas and limits
API access is part of your Jwab subscription. Each plan includes a number of API messages per month. Check usage at any time with Get usage.
When the monthly quota is used up the API returns 402 quota_exceeded, unless overage is turned on for your workspace.
Meta's WhatsApp conversation fees are billed separately, as they are today.
Rate limit: 600 requests per minute per key. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After.
How many customers you can message per day follows your number's Meta tier (1K, 10K, 100K or unlimited). Batches are paced to stay inside it.
Errors
Errors use standard HTTP status codes and always have the same shape. Use code in your logic and show message in logs. Quote request_id when you contact support.
Creates a template and submits it to Meta for approval. Status starts pending; you get a template.status_updated webhook when Meta decides. Needs scope templates:write.
Use {{1}}, {{2}} … for variables and give an example for each — Meta rejects templates without examples.
Request
curl -X POST 'https://api.jwab.ai/v1/templates' \
-H "Authorization: Bearer $JWAB_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f3e1c0a-9b2d-4e6f-8a1b-2c3d4e5f6a7b" \
-d '{
"name": "quotation_ready",
"language": "en",
"category": "utility",
"components": [
{
"type": "header",
"format": "document"
},
{
"type": "body",
"text": "Hello {{1}}, your quotation {{2}} for {{3}} is attached. It is valid until {{4}}.",
"examples": [
"Sara",
"QT-2026-0311",
"USD 4,800.00",
"2026-11-12"
]
},
{
"type": "footer",
"text": "Reply to this message to talk to our sales team"
},
{
"type": "buttons",
"buttons": [
{
"type": "url",
"text": "View quotation",
"url": "https://portal.acme.example/q/{{1}}",
"example": "QT-2026-0311"
},
{
"type": "quick_reply",
"text": "Accept"
},
{
"type": "quick_reply",
"text": "Request changes"
}
]
}
]
}'
{
"error": {
"type": "invalid_request_error",
"code": "quota_exceeded",
"message": "Your Business plan's 10,000 API messages for October are used. Upgrade or enable overage in Jwab → Billing.",
"request_id": "req_01JA8ZK3V9Q2",
"doc_url": "https://docs.jwab.ai/api/errors#quota_exceeded"
}
}
Send template — invoice PDF (file in the request)
POST/messages
Sends an invoice PDF with an approved template in ONE call: put the file itself in document.content as base64. This is the easiest way for an ERP that creates the PDF in memory.
Three ways to attach the PDF; use exactly one in document:
content — the file as base64, up to 15 MB. Nothing to host, one request.
url — an HTTPS link Jwab can download (a signed link valid for at least 1 hour is fine). See the next request.
media_id — upload first with Media → Upload media; best for files over 15 MB or one file sent to many customers.
Always send filename: it is the name the customer sees. WhatsApp accepts documents up to 100 MB.
buttons[].value fills the dynamic part of a URL button (e.g. https://pay.acme.example/{{1}}).
Returns 202 Accepted with the queued message. Track the result by webhook (message.sent/delivered/read/failed) or GET /messages/{id}.
{
"error": {
"type": "invalid_request_error",
"code": "invalid_document",
"message": "template.header.document.content is not a valid PDF (base64 could not be decoded, or the file is over 15 MB). For larger files upload them with POST /media and send media_id.",
"request_id": "req_01JA8ZK3V9Q2",
"doc_url": "https://docs.jwab.ai/api/errors#invalid_document",
"param": "template.header.document.content"
}
}
Send template — invoice PDF (link)
POST/messages
Same as above, but Jwab downloads the PDF from document.url. Use this when your system already stores invoices online. The link must answer within 10 seconds with the file (no login page); a signed link valid for at least 1 hour is fine. Jwab downloads it once, when the message is queued.
Returns 202 Accepted with the queued message. Track the result by webhook (message.sent/delivered/read/failed) or GET /messages/{id}.
{
"error": {
"type": "invalid_request_error",
"code": "document_unreachable",
"message": "Could not download template.header.document.url: the server answered 403. Use a public or signed link, or send the file in content.",
"request_id": "req_01JA8ZK3V9Q2",
"doc_url": "https://docs.jwab.ai/api/errors#document_unreachable",
"param": "template.header.document.url"
}
}
Send template — quotation with named parameters
POST/messages
Templates created with named variables ({{customer_name}}) take an object instead of a list. Quick‑reply buttons (Accept / Request changes) come back as message.received webhooks with button.payload, so your CRM can update the deal automatically.
Returns 202 Accepted with the queued message. Track the result by webhook (message.sent/delivered/read/failed) or GET /messages/{id}.
Authentication templates take the code once; Jwab fills the body and the copy‑code button. Set ttl_seconds so stale codes are dropped instead of delivered late.
Returns 202 Accepted with the queued message. Track the result by webhook (message.sent/delivered/read/failed) or GET /messages/{id}.
Free‑form text. Only allowed within 24 h of the customer's last message — otherwise 409 outside_service_window (send a template instead). Set preview_url to show a link preview.
Returns 202 Accepted with the queued message. Track the result by webhook (message.sent/delivered/read/failed) or GET /messages/{id}.
Request
curl -X POST 'https://api.jwab.ai/v1/messages' \
-H "Authorization: Bearer $JWAB_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f3e1c0a-9b2d-4e6f-8a1b-2c3d4e5f6a7b" \
-d '{
"channel_id": "ch_01J9X4F2",
"to": "+201001234567",
"type": "text",
"text": {
"body": "Thanks Ahmed, we received your payment for INV-2026-1042 ✅",
"preview_url": false
},
"client_reference": "PAY-55120"
}'
{
"error": {
"type": "invalid_request_error",
"code": "outside_service_window",
"message": "The customer's last message was 3 days ago. Use an approved template to start a new conversation.",
"request_id": "req_01JA8ZK3V9Q2",
"doc_url": "https://docs.jwab.ai/api/errors#outside_service_window"
}
}
Send document (inside 24h window)
POST/messages
Send a file without a template, e.g. when the customer asks for a statement in chat. Attach it the same three ways as a template document: content (base64), url or media_id. caption is optional.
Returns 202 Accepted with the queued message. Track the result by webhook (message.sent/delivered/read/failed) or GET /messages/{id}.
Send the same template to up to 1,000 recipients in one call — e.g. month‑end statements. Each recipient gets its own message and webhooks; per‑recipient errors don't fail the batch.
Create batch
POST/messages/batch
defaults apply to every item; each item overrides what differs. Sending is throttled to your Meta tier.
Keep Jwab contacts in sync with your CRM/ERP so agents see who they're talking to. service_window_open_until tells you whether a free‑form message is allowed right now.
Create or update contact (upsert by phone)
PUT/contacts/{phone}
Creates the contact if the phone is new, otherwise updates the fields you send. Tags are merged unless replace_tags is true.
Get delivery receipts, customer replies and template approvals pushed to your system in real time. The Event example requests show the payloads Jwab will POST to *your* URL — you can point your_webhook_url at your dev server to test your handler.
Create webhook endpoint
POST/webhooks
Register an HTTPS URL to receive events. The response contains the signing secretonce — store it.
Jwab sends a POST to your webhook URL for each event you subscribed to. Reply with any 2xx within 10 seconds. Failed deliveries are retried with backoff for 24 hours, so the same event can arrive twice: use event.id to ignore duplicates.
Event
When
message.sent
WhatsApp accepted the message.
message.delivered
The message reached the customer's phone.
message.read
The customer opened it (only if they have read receipts on).
message.failed
Sending failed. error.code says why; error.meta.code is Meta's code.
message.received
The customer wrote to you, or tapped a button. Includes the message you sent if it is a reply.
template.status_updated
Meta approved, rejected, paused or disabled one of your templates.
v1 is the HMAC‑SHA256 of t + "." + raw body, keyed with the endpoint's signing secret. Reject the request if the signature doesn't match or if t is more than 5 minutes old. When you rotate the secret, the old one keeps working for 24 hours.
Other headers: Jwab-Event (the event type) and Jwab-Delivery (unique per attempt).
Example
import crypto from 'node:crypto';
// Use the raw request body, before JSON parsing.
export function verifyJwab(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (age > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}