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
jwab.Business API v1 · design preview Postman collection
Jwab Business API · v1

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

  1. Connect your WhatsApp number in Jwab and get a utility template approved, for example invoice_ready with a document header and four variables.
  2. 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.
  3. Send the invoice:
curl -X POST 'https://api.jwab.ai/v1/messages' \
  -H "Authorization: Bearer $JWAB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: INV-2026-1042" \
  -d '{
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "template",
  "template": {
    "name": "invoice_ready",
    "language": "ar",
    "header": {
      "type": "document",
      "document": {
        "filename": "INV-2026-1042.pdf",
        "content": "JVBERi0xLjcKJcfsj6IK…"
      }
    },
    "body": [
      "أحمد حسن",
      "INV-2026-1042",
      "1,250.00 EGP",
      "2026-10-20"
    ]
  },
  "client_reference": "INV-2026-1042"
}'

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.
ScopeAllows
messages:sendSend messages and batches
messages:readRead message status and history
templates:readList and read templates
templates:writeCreate and delete templates
contacts:writeCreate, update and opt out contacts
media:writeUpload and delete media
webhooks:manageManage 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.
  • Template categories: utility (invoices, orders, account updates), marketing (offers), authentication (one‑time codes).
  • 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.

{
  "error": {
    "type": "invalid_request_error",
    "code": "template_not_approved",
    "message": "Template 'invoice_ready' (ar) is PENDING at Meta.",
    "param": "template.name",
    "request_id": "req_01JA8ZK3V9Q2",
    "doc_url": "https://docs.jwab.ai/api/errors#template_not_approved"
  }
}
HTTPMeaningCodes
400Invalid requestvalidation_failed invalid_phone_number template_parameter_mismatch invalid_document document_unreachable
401Missing or invalid API keyunauthenticated
402Subscription inactive or monthly quota used upsubscription_inactive quota_exceeded
403Key lacks the scope, or the IP is not allowedinsufficient_scope ip_not_allowed
404Not foundnot_found
409Not allowed right nowoutside_service_window contact_opted_out template_not_approved channel_disconnected idempotency_conflict
422WhatsApp rejected the message (Meta's code in error.meta)whatsapp_rejected
429Too many requests; wait for Retry-Afterrate_limited
5xxProblem on our side. Safe to retry with the same Idempotency-Keyapi_error

Getting started

Check that your key works and see which workspace and scopes it belongs to.

Who am I (verify API key)

GET/me

Returns the workspace, key and scopes for the key in Authorization. Use it as a health check from your ERP.

Request
curl -X GET 'https://api.jwab.ai/v1/me' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "object": "api_key",
  "id": "key_01JA7P0Q",
  "name": "Odoo production",
  "mode": "live",
  "scopes": [
    "messages:send",
    "messages:read",
    "templates:read",
    "media:write",
    "webhooks:manage"
  ],
  "workspace": {
    "id": "ws_44",
    "name": "Acme Trading",
    "timezone": "Africa/Cairo"
  },
  "created_at": "2026-10-01T08:00:00Z",
  "last_used_at": "2026-10-13T09:14:55Z"
}

Subscription & usage

Your API subscription and how much of this month's quota is used.

Get usage for current period

GET/usage

Plan, billing period and API message usage. remaining is null on unlimited plans.

Request
curl -X GET 'https://api.jwab.ai/v1/usage' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "object": "usage",
  "plan": {
    "code": "business",
    "name": "Business",
    "api_access": true
  },
  "subscription_status": "active",
  "period": {
    "start": "2026-10-01T00:00:00Z",
    "end": "2026-10-31T23:59:59Z"
  },
  "messages": {
    "included": 10000,
    "used": 3420,
    "remaining": 6580,
    "overage_enabled": false,
    "by_category": {
      "utility": 3100,
      "marketing": 250,
      "authentication": 70,
      "service": 0
    }
  },
  "api_requests_today": 5822
}

Channels (senders)

WhatsApp numbers connected to your workspace. Every send needs a channel_id.

List channels

GET/channels

All channels the key can send from, with WhatsApp quality rating and messaging tier.

Query parameters

NameExampleNotes
typewhatsappwhatsapp | messenger | instagram (only whatsapp supports templates today)
Request
curl -X GET 'https://api.jwab.ai/v1/channels?type=whatsapp' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "object": "list",
  "has_more": false,
  "next_cursor": null,
  "data": [
    {
      "id": "ch_01J9X4F2",
      "object": "channel",
      "type": "whatsapp",
      "name": "Acme Sales",
      "phone_number": "+20225550100",
      "status": "connected",
      "quality_rating": "GREEN",
      "messaging_limit_tier": "TIER_10K",
      "default": true
    }
  ]
}

Get channel

GET/channels/{id}

One channel by ID.

Request
curl -X GET 'https://api.jwab.ai/v1/channels/ch_01J9X4F2' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "id": "ch_01J9X4F2",
  "object": "channel",
  "type": "whatsapp",
  "name": "Acme Sales",
  "phone_number": "+20225550100",
  "status": "connected",
  "quality_rating": "GREEN",
  "messaging_limit_tier": "TIER_10K",
  "default": true
}

Templates

Meta‑approved message templates. Only approved templates can be sent. The components show exactly which parameters each send needs.

List templates

GET/templates

Filter by status, category or language.

Query parameters

NameExampleNotes
statusapprovedapproved | pending | rejected | paused | disabled
categoryutilityutility | marketing | authentication
languagearar | en | …
limit25
cursor—
Request
curl -X GET 'https://api.jwab.ai/v1/templates?status=approved' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "object": "list",
  "has_more": false,
  "next_cursor": null,
  "data": [
    {
      "id": "tpl_01J9Y0AB",
      "object": "template",
      "name": "invoice_ready",
      "language": "ar",
      "category": "utility",
      "status": "approved",
      "rejection_reason": null,
      "components": [
        {
          "type": "header",
          "format": "document"
        },
        {
          "type": "body",
          "text": "مرحباً {{1}}، فاتورتك رقم {{2}} بقيمة {{3}} جاهزة. تاريخ الاستحقاق {{4}}.",
          "parameters": [
            {
              "index": 1,
              "example": "أحمد"
            },
            {
              "index": 2,
              "example": "INV-2026-1042"
            },
            {
              "index": 3,
              "example": "1,250.00 EGP"
            },
            {
              "index": 4,
              "example": "2026-10-20"
            }
          ]
        },
        {
          "type": "footer",
          "text": "Acme Trading"
        },
        {
          "type": "buttons",
          "buttons": [
            {
              "index": 0,
              "type": "url",
              "text": "ادفع الآن",
              "url": "https://pay.acme.example/{{1}}",
              "example": "INV-2026-1042"
            }
          ]
        }
      ],
      "channel_id": "ch_01J9X4F2",
      "updated_at": "2026-10-02T11:20:00Z"
    }
  ]
}

Get template

GET/templates/{name}

Get by name (all languages) or by ID (tpl_…). Add ?language=ar for one language.

Query parameters

NameExampleNotes
languagear
Request
curl -X GET 'https://api.jwab.ai/v1/templates/invoice_ready?language=ar' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "id": "tpl_01J9Y0AB",
  "object": "template",
  "name": "invoice_ready",
  "language": "ar",
  "category": "utility",
  "status": "approved",
  "rejection_reason": null,
  "components": [
    {
      "type": "header",
      "format": "document"
    },
    {
      "type": "body",
      "text": "مرحباً {{1}}، فاتورتك رقم {{2}} بقيمة {{3}} جاهزة. تاريخ الاستحقاق {{4}}.",
      "parameters": [
        {
          "index": 1,
          "example": "أحمد"
        },
        {
          "index": 2,
          "example": "INV-2026-1042"
        },
        {
          "index": 3,
          "example": "1,250.00 EGP"
        },
        {
          "index": 4,
          "example": "2026-10-20"
        }
      ]
    },
    {
      "type": "footer",
      "text": "Acme Trading"
    },
    {
      "type": "buttons",
      "buttons": [
        {
          "index": 0,
          "type": "url",
          "text": "ادفع الآن",
          "url": "https://pay.acme.example/{{1}}",
          "example": "INV-2026-1042"
        }
      ]
    }
  ],
  "channel_id": "ch_01J9X4F2",
  "updated_at": "2026-10-02T11:20:00Z"
}

Create template (submit to Meta)

POST/templates

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"
        }
      ]
    }
  ]
}'
Response
{
  "id": "tpl_01JA90CD",
  "object": "template",
  "name": "quotation_ready",
  "language": "en",
  "category": "utility",
  "status": "pending",
  "created_at": "2026-10-13T09:30:00Z"
}

Delete template

DELETE/templates/{name}

Deletes the template (all languages) in Jwab and at Meta. Meta blocks re‑using the same name for 30 days.

Request
curl -X DELETE 'https://api.jwab.ai/v1/templates/invoice_ready' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "id": "tpl_01JA90CD",
  "object": "template",
  "deleted": true
}

Messages

Send templates and session messages, check status, list history.

Fields on every send

  • channel_id — optional if your workspace has a default channel.
  • to — customer phone, E.164.
  • type — template, text, document, image, video, audio, location, interactive.
  • client_reference (≤ 128 chars) and metadata (≤ 20 keys) — your own IDs, echoed back everywhere.
  • contact — optional; creates/updates the contact in Jwab (name, email, tags, custom fields).
  • conversation — optional; assign_to_department / assign_to_user so replies land with the right team, and note shown to agents.

Send template — simple text (payment reminder)

POST/messages

The smallest valid template send: body variables in order.

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": "template",
  "template": {
    "name": "payment_reminder",
    "language": "ar",
    "body": [
      "أحمد",
      "INV-2026-1042",
      "1,250.00 EGP",
      "2026-10-20"
    ]
  },
  "client_reference": "INV-2026-1042"
}'
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "queued",
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "template",
  "template": {
    "name": "payment_reminder",
    "language": "ar",
    "category": "utility"
  },
  "client_reference": "INV-2026-1042",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z"
}

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}.

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": "template",
  "template": {
    "name": "invoice_ready",
    "language": "ar",
    "header": {
      "type": "document",
      "document": {
        "filename": "فاتورة INV-2026-1042.pdf",
        "content": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9GaWx0ZXIvRmxhdGVEZWNvZGUvTGVuZ3RoIDM4Pj4Kc3RyZWFtCnicK+QyVDAAQXNDE…"
      }
    },
    "body": [
      "أحمد حسن",
      "INV-2026-1042",
      "1,250.00 EGP",
      "2026-10-20"
    ],
    "buttons": [
      {
        "index": 0,
        "type": "url",
        "value": "INV-2026-1042"
      }
    ]
  },
  "contact": {
    "name": "Ahmed Hassan",
    "email": "ahmed@example.com",
    "tags": [
      "customer",
      "odoo"
    ],
    "custom_fields": {
      "customer_code": "C-00981"
    }
  },
  "conversation": {
    "assign_to_department": "Accounts",
    "note": "Invoice INV-2026-1042 sent from Odoo"
  },
  "client_reference": "INV-2026-1042",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042",
    "amount": "1250.00",
    "currency": "EGP"
  }
}'
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "queued",
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "template",
  "template": {
    "name": "invoice_ready",
    "language": "ar",
    "category": "utility"
  },
  "client_reference": "INV-2026-1042",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z"
}

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}.

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": "+971501234567",
  "type": "template",
  "template": {
    "name": "quotation_ready_v2",
    "language": "en",
    "header": {
      "type": "document",
      "document": {
        "media_id": "med_01JA9C1F",
        "filename": "QT-2026-0311.pdf"
      }
    },
    "body": {
      "customer_name": "Sara",
      "quote_no": "QT-2026-0311",
      "total": "USD 4,800.00",
      "valid_until": "12 Nov 2026"
    },
    "buttons": [
      {
        "index": 0,
        "type": "url",
        "value": "QT-2026-0311"
      },
      {
        "index": 1,
        "type": "quick_reply",
        "payload": "quote_accept:QT-2026-0311"
      },
      {
        "index": 2,
        "type": "quick_reply",
        "payload": "quote_changes:QT-2026-0311"
      }
    ]
  },
  "client_reference": "QT-2026-0311",
  "metadata": {
    "source": "hubspot",
    "deal_id": "9876543210"
  }
}'
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "queued",
  "channel_id": "ch_01J9X4F2",
  "to": "+971501234567",
  "type": "template",
  "template": {
    "name": "quotation_ready_v2",
    "language": "en",
    "category": "utility"
  },
  "client_reference": "QT-2026-0311",
  "metadata": {
    "source": "hubspot",
    "deal_id": "9876543210"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z"
}

Send template — image header (order shipped)

POST/messages

Image (image) and video (video) headers work the same way as documents.

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 '{
  "to": "+201001234567",
  "type": "template",
  "template": {
    "name": "order_shipped",
    "language": "en",
    "header": {
      "type": "image",
      "image": {
        "url": "https://cdn.acme.example/orders/58231.jpg"
      }
    },
    "body": [
      "Mona",
      "#58231",
      "Aramex",
      "AWB 3349981120"
    ],
    "buttons": [
      {
        "index": 0,
        "type": "url",
        "value": "3349981120"
      }
    ]
  },
  "client_reference": "SO-58231"
}'
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "queued",
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "template",
  "template": {
    "name": "invoice_ready",
    "language": "ar",
    "category": "utility"
  },
  "client_reference": "SO-58231",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z"
}

Send template — OTP (authentication)

POST/messages

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}.

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 '{
  "to": "+201001234567",
  "type": "template",
  "template": {
    "name": "login_code",
    "language": "en",
    "code": "482913"
  },
  "ttl_seconds": 300,
  "client_reference": "login-77812"
}'
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "queued",
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "template",
  "template": {
    "name": "login_code",
    "language": "en",
    "category": "authentication"
  },
  "client_reference": "login-77812",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z"
}

Send text (inside 24h window)

POST/messages

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"
}'
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "queued",
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "text",
  "template": null,
  "client_reference": "PAY-55120",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z"
}

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}.

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 '{
  "to": "+201001234567",
  "type": "document",
  "document": {
    "url": "https://erp.acme.example/files/statement-C-00981-2026-09.pdf",
    "filename": "Statement Sep 2026.pdf",
    "caption": "Your account statement for September"
  },
  "client_reference": "STMT-C-00981-202609"
}'
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "queued",
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "document",
  "template": null,
  "client_reference": "STMT-C-00981-202609",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z"
}

Send image (inside 24h window)

POST/messages

JPEG/PNG up to 5 MB.

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 '{
  "to": "+201001234567",
  "type": "image",
  "image": {
    "media_id": "med_01JA9C1F",
    "caption": "Product photo — SKU A-1190"
  }
}'
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "queued",
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "image",
  "template": null,
  "client_reference": "INV-2026-1042",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z"
}

Send interactive buttons (inside 24h window)

POST/messages

Up to 3 reply buttons. The customer's tap arrives as message.received with interactive.button_reply.id.

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 '{
  "to": "+201001234567",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": {
      "text": "Would you like to confirm delivery for tomorrow 10:00–14:00?"
    },
    "buttons": [
      {
        "id": "deliv_ok:SO-58231",
        "title": "Confirm"
      },
      {
        "id": "deliv_resched:SO-58231",
        "title": "Reschedule"
      }
    ]
  }
}'
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "queued",
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "interactive",
  "template": null,
  "client_reference": "INV-2026-1042",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z"
}

Get message

GET/messages/{id}

Current status, timestamps and failure reason.

Request
curl -X GET 'https://api.jwab.ai/v1/messages/msg_01JA8ZM4T6R1XK' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "id": "msg_01JA8ZM4T6R1XK",
  "object": "message",
  "status": "read",
  "channel_id": "ch_01J9X4F2",
  "to": "+201001234567",
  "type": "template",
  "template": {
    "name": "invoice_ready",
    "language": "ar",
    "category": "utility"
  },
  "client_reference": "INV-2026-1042",
  "metadata": {
    "source": "odoo",
    "invoice_id": "1042"
  },
  "conversation_id": "conv_01JA8ZM4T7",
  "contact_id": "ct_01JA7Q2B9X",
  "created_at": "2026-10-13T09:15:02Z",
  "wa_message_id": "wamid.HBgMMjAxMDAxMjM0NTY3FQIAERgSQjA2",
  "pricing": {
    "billable": true,
    "category": "utility"
  },
  "timeline": {
    "queued_at": "2026-10-13T09:15:02Z",
    "sent_at": "2026-10-13T09:15:03Z",
    "delivered_at": "2026-10-13T09:15:05Z",
    "read_at": "2026-10-13T09:17:41Z",
    "failed_at": null
  },
  "error": null
}

List messages

GET/messages

Search outbound and inbound messages.

Query parameters

NameExampleNotes
client_referenceINV-2026-1042Exact match on your reference
statusfailedqueued | sent | delivered | read | failed
to+201001234567
directionoutboundoutbound | inbound
created_after2026-10-01T00:00:00Z
limit25
cursor—
Request
curl -X GET 'https://api.jwab.ai/v1/messages?client_reference=INV-2026-1042' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "object": "list",
  "has_more": false,
  "next_cursor": null,
  "data": [
    {
      "id": "msg_01JA8ZM4T6R1XK",
      "object": "message",
      "status": "read",
      "channel_id": "ch_01J9X4F2",
      "to": "+201001234567",
      "type": "template",
      "template": {
        "name": "invoice_ready",
        "language": "ar",
        "category": "utility"
      },
      "client_reference": "INV-2026-1042",
      "metadata": {
        "source": "odoo",
        "invoice_id": "1042"
      },
      "conversation_id": "conv_01JA8ZM4T7",
      "contact_id": "ct_01JA7Q2B9X",
      "created_at": "2026-10-13T09:15:02Z",
      "wa_message_id": "wamid.HBgMMjAxMDAxMjM0NTY3FQIAERgSQjA2",
      "pricing": {
        "billable": true,
        "category": "utility"
      },
      "timeline": {
        "queued_at": "2026-10-13T09:15:02Z",
        "sent_at": "2026-10-13T09:15:03Z",
        "delivered_at": "2026-10-13T09:15:05Z",
        "read_at": "2026-10-13T09:17:41Z",
        "failed_at": null
      },
      "error": null
    }
  ]
}

Bulk sending (batches)

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.

Request
curl -X POST 'https://api.jwab.ai/v1/messages/batch' \
  -H "Authorization: Bearer $JWAB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3e1c0a-9b2d-4e6f-8a1b-2c3d4e5f6a7b" \
  -d '{
  "channel_id": "ch_01J9X4F2",
  "defaults": {
    "type": "template",
    "template": {
      "name": "statement_ready",
      "language": "ar"
    }
  },
  "messages": [
    {
      "to": "+201001234567",
      "template": {
        "header": {
          "type": "document",
          "document": {
            "url": "https://erp.acme.example/st/C-00981.pdf",
            "filename": "كشف حساب.pdf"
          }
        },
        "body": [
          "أحمد",
          "سبتمبر 2026",
          "3,400.00 EGP"
        ]
      },
      "client_reference": "ST-C-00981-202609"
    },
    {
      "to": "+201112223334",
      "template": {
        "header": {
          "type": "document",
          "document": {
            "url": "https://erp.acme.example/st/C-01007.pdf",
            "filename": "كشف حساب.pdf"
          }
        },
        "body": [
          "منى",
          "سبتمبر 2026",
          "0.00 EGP"
        ]
      },
      "client_reference": "ST-C-01007-202609"
    }
  ],
  "client_reference": "statements-2026-09"
}'
Response
{
  "id": "bat_01JA9B7E",
  "object": "batch",
  "status": "processing",
  "total": 2,
  "accepted": 2,
  "rejected": 0,
  "rejections": [],
  "client_reference": "statements-2026-09",
  "created_at": "2026-10-13T10:00:00Z"
}

Get batch

GET/messages/batch/{id}

Progress counters. Use List messages with batch_id for per‑recipient detail.

Request
curl -X GET 'https://api.jwab.ai/v1/messages/batch/bat_01JA9B7E' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "id": "bat_01JA9B7E",
  "object": "batch",
  "status": "completed",
  "total": 2,
  "counts": {
    "queued": 0,
    "sent": 0,
    "delivered": 1,
    "read": 1,
    "failed": 0
  },
  "client_reference": "statements-2026-09",
  "completed_at": "2026-10-13T10:00:09Z"
}

Media

Upload a file once and reuse its media_id in many sends (kept 30 days). Alternatively pass a url directly in the message.

Upload media

POST/media

multipart/form-data. Documents up to 100 MB (PDF, DOCX, XLSX…), images 5 MB (JPEG/PNG), video 16 MB (MP4), audio 16 MB.

Form fields

fileThe file to upload
purposewhatsapp
Request
curl -X POST 'https://api.jwab.ai/v1/media' \
  -H "Authorization: Bearer $JWAB_API_KEY" \
  -F "file=@QT-2026-0311.pdf" \
  -F "purpose=whatsapp"
Response
{
  "id": "med_01JA9C1F",
  "object": "media",
  "filename": "QT-2026-0311.pdf",
  "mime_type": "application/pdf",
  "size": 284113,
  "expires_at": "2026-11-12T09:40:00Z"
}

Get media

GET/media/{id}

Metadata plus a short‑lived download_url (also used for files customers send you).

Request
curl -X GET 'https://api.jwab.ai/v1/media/med_01JA9C1F' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "id": "med_01JA9C1F",
  "object": "media",
  "filename": "QT-2026-0311.pdf",
  "mime_type": "application/pdf",
  "size": 284113,
  "download_url": "https://files.jwab.ai/m/med_01JA9C1F?exp=1760350000&sig=…",
  "expires_at": "2026-11-12T09:40:00Z"
}

Delete media

DELETE/media/{id}
Request
curl -X DELETE 'https://api.jwab.ai/v1/media/med_01JA9C1F' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "id": "med_01JA9C1F",
  "object": "media",
  "deleted": true
}

Contacts

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.

Request
curl -X PUT 'https://api.jwab.ai/v1/contacts/+201001234567' \
  -H "Authorization: Bearer $JWAB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Ahmed Hassan",
  "email": "ahmed@example.com",
  "tags": [
    "customer",
    "odoo"
  ],
  "custom_fields": {
    "customer_code": "C-00981",
    "city": "Cairo"
  },
  "language": "ar"
}'
Response
{
  "id": "ct_01JA7Q2B9X",
  "object": "contact",
  "phone": "+201001234567",
  "name": "Ahmed Hassan",
  "email": "ahmed@example.com",
  "tags": [
    "customer",
    "odoo"
  ],
  "custom_fields": {
    "customer_code": "C-00981"
  },
  "marketing_opt_out": false,
  "blocked": false,
  "last_inbound_at": "2026-10-12T18:03:00Z",
  "service_window_open_until": "2026-10-13T18:03:00Z",
  "created_at": "2026-09-01T10:00:00Z"
}

Get contact

GET/contacts/{phone}

By phone (E.164) or by ct_… ID.

Request
curl -X GET 'https://api.jwab.ai/v1/contacts/+201001234567' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "id": "ct_01JA7Q2B9X",
  "object": "contact",
  "phone": "+201001234567",
  "name": "Ahmed Hassan",
  "email": "ahmed@example.com",
  "tags": [
    "customer",
    "odoo"
  ],
  "custom_fields": {
    "customer_code": "C-00981"
  },
  "marketing_opt_out": false,
  "blocked": false,
  "last_inbound_at": "2026-10-12T18:03:00Z",
  "service_window_open_until": "2026-10-13T18:03:00Z",
  "created_at": "2026-09-01T10:00:00Z"
}

List contacts

GET/contacts

Query parameters

NameExampleNotes
tagodoo
updated_after2026-10-01T00:00:00Z
limit25
cursor—
Request
curl -X GET 'https://api.jwab.ai/v1/contacts?tag=odoo' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "object": "list",
  "has_more": false,
  "next_cursor": null,
  "data": [
    {
      "id": "ct_01JA7Q2B9X",
      "object": "contact",
      "phone": "+201001234567",
      "name": "Ahmed Hassan",
      "email": "ahmed@example.com",
      "tags": [
        "customer",
        "odoo"
      ],
      "custom_fields": {
        "customer_code": "C-00981"
      },
      "marketing_opt_out": false,
      "blocked": false,
      "last_inbound_at": "2026-10-12T18:03:00Z",
      "service_window_open_until": "2026-10-13T18:03:00Z",
      "created_at": "2026-09-01T10:00:00Z"
    }
  ]
}

Set marketing opt‑out

POST/contacts/{phone}/opt-out

Record that the customer doesn't want marketing messages (e.g. unsubscribed in your CRM). Use DELETE on the same path to opt back in.

Request
curl -X POST 'https://api.jwab.ai/v1/contacts/+201001234567/opt-out' \
  -H "Authorization: Bearer $JWAB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3e1c0a-9b2d-4e6f-8a1b-2c3d4e5f6a7b" \
  -d '{
  "reason": "Unsubscribed via CRM"
}'
Response
{
  "id": "ct_01JA7Q2B9X",
  "object": "contact",
  "phone": "+201001234567",
  "name": "Ahmed Hassan",
  "email": "ahmed@example.com",
  "tags": [
    "customer",
    "odoo"
  ],
  "custom_fields": {
    "customer_code": "C-00981"
  },
  "marketing_opt_out": true,
  "blocked": false,
  "last_inbound_at": "2026-10-12T18:03:00Z",
  "service_window_open_until": "2026-10-13T18:03:00Z",
  "created_at": "2026-09-01T10:00:00Z"
}

Webhooks

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 secret once — store it.

Request
curl -X POST 'https://api.jwab.ai/v1/webhooks' \
  -H "Authorization: Bearer $JWAB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3e1c0a-9b2d-4e6f-8a1b-2c3d4e5f6a7b" \
  -d '{
  "url": "https://erp.acme.example/hooks/jwab",
  "description": "Odoo production",
  "events": [
    "message.sent",
    "message.delivered",
    "message.read",
    "message.failed",
    "message.received",
    "template.status_updated"
  ]
}'
Response
{
  "id": "wh_01JA9D2G",
  "object": "webhook_endpoint",
  "url": "https://erp.acme.example/hooks/jwab",
  "events": [
    "message.sent",
    "message.delivered",
    "message.read",
    "message.failed",
    "message.received",
    "template.status_updated"
  ],
  "status": "enabled",
  "description": "Odoo production",
  "created_at": "2026-10-13T09:50:00Z",
  "secret": "whsec_9d7Hq2…"
}

List webhook endpoints

GET/webhooks
Request
curl -X GET 'https://api.jwab.ai/v1/webhooks' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "object": "list",
  "has_more": false,
  "next_cursor": null,
  "data": [
    {
      "id": "wh_01JA9D2G",
      "object": "webhook_endpoint",
      "url": "https://erp.acme.example/hooks/jwab",
      "events": [
        "message.sent",
        "message.delivered",
        "message.read",
        "message.failed",
        "message.received",
        "template.status_updated"
      ],
      "status": "enabled",
      "description": "Odoo production",
      "created_at": "2026-10-13T09:50:00Z"
    }
  ]
}

Update webhook endpoint

PATCH/webhooks/{id}

Change URL, events, or set status to disabled.

Request
curl -X PATCH 'https://api.jwab.ai/v1/webhooks/wh_01JA9D2G' \
  -H "Authorization: Bearer $JWAB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "events": [
    "message.delivered",
    "message.read",
    "message.failed",
    "message.received"
  ]
}'
Response
{
  "id": "wh_01JA9D2G",
  "object": "webhook_endpoint",
  "url": "https://erp.acme.example/hooks/jwab",
  "events": [
    "message.delivered",
    "message.read",
    "message.failed",
    "message.received"
  ],
  "status": "enabled",
  "description": "Odoo production",
  "created_at": "2026-10-13T09:50:00Z"
}

Send test event

POST/webhooks/{id}/test

Sends a signed webhook.test event to the endpoint and returns your server's response.

Request
curl -X POST 'https://api.jwab.ai/v1/webhooks/wh_01JA9D2G/test' \
  -H "Authorization: Bearer $JWAB_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3e1c0a-9b2d-4e6f-8a1b-2c3d4e5f6a7b" \
  -d '{
  "event": "message.delivered"
}'
Response
{
  "delivered": true,
  "response_status": 200,
  "duration_ms": 184
}

Rotate signing secret

POST/webhooks/{id}/rotate-secret

Issues a new secret. The old one keeps working for 24 h so you can deploy without dropping events.

Request
curl -X POST 'https://api.jwab.ai/v1/webhooks/wh_01JA9D2G/rotate-secret' \
  -H "Authorization: Bearer $JWAB_API_KEY" \
  -H "Idempotency-Key: 7f3e1c0a-9b2d-4e6f-8a1b-2c3d4e5f6a7b"
Response
{
  "id": "wh_01JA9D2G",
  "secret": "whsec_X1b0…",
  "previous_secret_expires_at": "2026-10-14T10:00:00Z"
}

Delete webhook endpoint

DELETE/webhooks/{id}
Request
curl -X DELETE 'https://api.jwab.ai/v1/webhooks/wh_01JA9D2G' \
  -H "Authorization: Bearer $JWAB_API_KEY"
Response
{
  "id": "wh_01JA9D2G",
  "object": "webhook_endpoint",
  "deleted": true
}
Webhooks

Events and payloads

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.

EventWhen
message.sentWhatsApp accepted the message.
message.deliveredThe message reached the customer's phone.
message.readThe customer opened it (only if they have read receipts on).
message.failedSending failed. error.code says why; error.meta.code is Meta's code.
message.receivedThe customer wrote to you, or tapped a button. Includes the message you sent if it is a reply.
template.status_updatedMeta approved, rejected, paused or disabled one of your templates.
Example payloads
{
  "id": "evt_01JA9EMESS",
  "object": "event",
  "type": "message.delivered",
  "created_at": "2026-10-13T09:15:05Z",
  "workspace_id": "ws_44",
  "livemode": true,
  "data": {
    "message": {
      "id": "msg_01JA8ZM4T6R1XK",
      "status": "delivered",
      "to": "+201001234567",
      "client_reference": "INV-2026-1042",
      "metadata": {
        "source": "odoo",
        "invoice_id": "1042"
      },
      "delivered_at": "2026-10-13T09:15:05Z"
    }
  }
}

Verify signatures

Every request carries a Jwab-Signature header:

Jwab-Signature: t=1760346905,v1=5f2b9c…

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));
}