WhatsApp

Register a sender, get templates approved by Meta, and message contacts inside and outside the 24-hour window.

View as Markdown

Jelliu agents answer and start conversations on WhatsApp Business. The channel runs on the WhatsApp Business Platform through Twilio, with one sender (a business phone number) per workspace. Everything a contact writes to that number is routed to one of your agents, and everything you send goes through the same compliance, opt-out and quota checks as the rest of the platform.

This page covers the API surface: senders, templates, sending, what happens to inbound messages, and the limits that apply.

How it works

Three rules shape every integration:

  1. Nothing is sent without an ONLINE sender. Sends from a workspace whose number is still being approved, or is offline, are refused with an explanation.
  2. Meta’s 24-hour customer-care window decides the message form. Freeform text is allowed only if the contact wrote to your number in the last 24 hours. Outside that window only an approved template can be sent.
  3. An opt-out is final. When a contact asks to stop, Jelliu suppresses the number and sends nothing more, not even a confirmation.

Senders

A sender is the WhatsApp Business phone number your workspace messages from. Replies are sent from the line the contact last wrote to, when that line is still online; otherwise from your most recently updated online sender.

Endpoints

MethodPathAPI key scope
GET/api/whatsapp-sendersread
GET/api/whatsapp-senders/{senderId}read
GET/api/whatsapp-senders/verticalsread
GET/api/whatsapp-senders/adoptableread
POST/api/whatsapp-sendersfull
POST/api/whatsapp-senders/{senderId}/verifyfull
PATCH/api/whatsapp-senders/{senderId}full
POST/api/whatsapp-senders/{senderId}/refreshfull
POST/api/whatsapp-senders/adoptfull
DELETE/api/whatsapp-senders/{senderId}full

Every response wraps the result in data. DELETE returns 204 with no body.

Sender lifecycle

StatusMeaning
CREATINGRegistration was accepted and is being set up.
VERIFYINGWaiting for the one-time code sent to the number, or for review.
ONLINECan send and receive.
OFFLINERegistered but not usable. status_reason says why.
FAILEDRegistration did not complete.

Twilio does not push sender status changes, so Jelliu polls them about every two minutes and also re-checks senders that are already ONLINE, because Meta can take a number offline for quality reasons. Call POST /api/whatsapp-senders/{senderId}/refresh to re-read the status immediately.

status_reason is written for the person who has to fix it and may be in Spanish. For example, a number still registered on another WhatsApp account reports: Este número sigue registrado en otra cuenta de WhatsApp. Elimina esa cuenta en el teléfono (Ajustes → Cuenta → Eliminar cuenta) o migra el número, espera unos minutos y vuelve a intentarlo.

The sender object

FieldTypeDescription
iduuidSender ID used in the paths above.
phone_numberstringE.164 number, for example +573001234567.
statusstringOne of the lifecycle statuses.
status_reasonstring or nullWhy the sender is not online, when known.
display_namestring or nullBusiness name shown in the chat header.
about_textstring or nullProfile “about” text.
waba_idstringThe WhatsApp Business Account the sender belongs to.
twilio_sender_sidstring or nullProvider identifier, useful for support.
profile, configurationobject or nullBusiness profile and provider configuration as last read.
created_at, updated_atISO 8601Timestamps.

Register a number

phoneNumber
stringRequired

E.164 format: +, a country digit 1 to 9, then up to 14 more digits.

verificationMethod
string

sms or voice: how the one-time code is delivered to the number.

wabaId
string

Your own WhatsApp Business Account ID, up to 64 characters. Omit it to register under the account connected to the platform.

phoneNumberId
string

Meta’s ID for the number inside that account, up to 64 characters. Recorded for support only.

displayName
string

1 to 120 characters.

aboutText
string

Up to 139 characters.

website
string

A URL, up to 256 characters.

email
string

An email address, up to 128 characters.

address
string

Up to 256 characters.

description
string

Up to 256 characters.

vertical
string

One of Meta’s fixed business categories. GET /api/whatsapp-senders/verticals returns the list: Automotive, Beauty, Spa and Salon, Clothing and Apparel, Education, Entertainment, Event Planning and Service, Finance and Banking, Food and Grocery, Public Service, Hotel and Lodging, Medical and Health, Non-profit, Professional Services, Shopping and Retail, Travel and Transportation, Restaurant, Other.

logoUrl
string

A URL, up to 512 characters.

1

Create the sender

curl -sS -X POST "https://api.jelliu.co/api/whatsapp-senders" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+573001234567",
"verificationMethod": "sms",
"displayName": "Clínica Norte",
"vertical": "Medical and Health",
"website": "https://clinicanorte.example.com"
}'

The response is 201 with the sender, typically in CREATING or VERIFYING.

2

Submit the one-time code

When the code arrives on the number, send it as code (3 to 10 digits):

curl -sS -X POST "https://api.jelliu.co/api/whatsapp-senders/$SENDER_ID/verify" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "482913" }'
3

Wait for ONLINE

Poll GET /api/whatsapp-senders/{senderId} (or call /refresh) until status is ONLINE. Inbound messages are routed to your workspace only once the sender is online.

Do not delete an ONLINE sender to “test reconnecting”. Removing it at the provider does not remove the number from the WhatsApp Business Account, and registering it again can be blocked by Meta, for example when two-step verification is enabled on the number in WhatsApp Manager.

Claim an existing sender

If a number is already registered as a WhatsApp sender with the provider, POST /api/whatsapp-senders/adopt with { "phoneNumber": "+573001234567" } binds it to your workspace. GET /api/whatsapp-senders/adoptable lists candidates. The number must already be an active number of your workspace; otherwise the claim is refused with 403: +573001234567 is not one of this account's numbers, so it cannot be claimed as a WhatsApp sender.

Update and remove

PATCH /api/whatsapp-senders/{senderId} accepts any of the profile fields above (at least one). It fails with 400 while the sender is not yet registered with the provider.

DELETE /api/whatsapp-senders/{senderId} removes the sender at the provider first. If the provider refuses, nothing is changed and you can retry.

Templates

A template is a message Meta has approved in advance. It is the only way to write to someone who has not messaged your number in the last 24 hours, and it also works inside the window.

Endpoints

MethodPathAPI key scope
GET/api/whatsapp-templatesread
GET/api/whatsapp-templates/{templateId}read
GET/api/whatsapp-templates/{templateId}/previewread
POST/api/whatsapp-templates/{templateId}/previewwrite
POST/api/whatsapp-templatesfull
PATCH/api/whatsapp-templates/{templateId}full
POST/api/whatsapp-templates/{templateId}/submitfull
POST/api/whatsapp-templates/{templateId}/refreshfull
POST/api/whatsapp-templates/{templateId}/test-sendfull
DELETE/api/whatsapp-templates/{templateId}full
GET/api/whatsapp-templates/adoptableread
POST/api/whatsapp-templates/adoptfull
POST/api/whatsapp-templates/meta/listfull
POST/api/whatsapp-templates/meta/importfull

Categories

CategoryUse it for
UTILITYDefault. Transactional follow-ups: confirmations, reminders, updates.
MARKETINGPromotions and re-engagement.
AUTHENTICATIONOne-time codes.

Approval states

StatusMeaningEditableSendable
draftCreated, not submitted.YesNo
pendingSubmitted and waiting for Meta.NoNo
approvedApproved by Meta.NoYes
rejectedRejected. rejection_reason explains why, when Meta gives one.YesNo
pausedPaused by Meta, usually for quality.NoNo
disabledDisabled by Meta.NoNo

Approval results are polled from the provider about every two minutes, including for templates already approved, because Meta can pause or disable them later. POST /api/whatsapp-templates/{templateId}/refresh re-reads the state on demand.

The template object

FieldTypeDescription
iduuidTemplate ID, used as templateId when sending.
namestringImmutable identifier: lowercase letters, digits and underscores, up to 120 characters. Unique per language in your workspace.
friendly_namestring or nullDisplay label you can rename.
languagestringLanguage tag, for example es, en or pt-BR.
categorystringUTILITY, MARKETING or AUTHENTICATION.
body_textstringThe message body, with numbered placeholders.
variablesstring[]One label per placeholder, in order.
statusstringApproval state.
rejection_reasonstring or nullMeta’s reason, when rejected.
twilio_content_sidstring or nullProvider content ID, set when submitted.
submitted_at, approved_atISO 8601 or nullWhen it was submitted and approved.
created_at, updated_atISO 8601Timestamps.

Variables

Placeholders are written {{1}}, {{2}} and so on. variables declares one label per placeholder, so a body with two placeholders needs exactly two labels. Before a template is created, edited or submitted, Jelliu checks the body against the rules Meta rejects on, so you find out in seconds rather than after days of review:

  • numbering starts at {{1}} and has no gaps;
  • the body is not only a placeholder;
  • two placeholders are never adjacent ({{1}} {{2}});
  • the body does not start or end with a placeholder;
  • the number of labels matches the number of placeholders.

A body that breaks a rule is refused with 400 and code VALIDATION_FAILED, and the message names the rule, in Spanish. For example:

{
"error": {
"code": "VALIDATION_FAILED",
"message": "WhatsApp rechazaria esta plantilla: El mensaje no puede TERMINAR con una variable: pon texto después de la última variable."
}
}
name
stringRequired

Matches ^[a-z0-9_]{1,120}$.

bodyText
stringRequired

1 to 1024 characters.

language
stringDefaults to es

2 to 16 characters.

category
stringDefaults to UTILITY

AUTHENTICATION, MARKETING or UTILITY.

variables
string[]Defaults to []

Up to 20 labels, each 1 to 50 characters.

friendlyName
string

Up to 200 characters.

Create, submit and preview a template

1

Create a draft

curl -sS -X POST "https://api.jelliu.co/api/whatsapp-templates" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "recordatorio_cita",
"friendlyName": "Recordatorio de cita",
"language": "es",
"category": "UTILITY",
"bodyText": "Hola {{1}}, te recordamos tu cita del {{2}} en Clínica Norte. Responde a este mensaje si necesitas cambiarla.",
"variables": ["nombre", "fecha"]
}'
2

Preview it

Preview renders the body with sample values. It never contacts the provider and costs nothing. Missing values appear as [label], and warnings lists anything Meta would reject.

curl -sS -X POST "https://api.jelliu.co/api/whatsapp-templates/$TEMPLATE_ID/preview" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "variables": ["Ana", "viernes 19 a las 10:00"] }'
{
"data": {
"templateId": "3f1c9a52-7b0e-4d3a-9c21-5e8f7a6b4d10",
"name": "recordatorio_cita",
"language": "es",
"bodyText": "Hola {{1}}, te recordamos tu cita del {{2}} en Clínica Norte. Responde a este mensaje si necesitas cambiarla.",
"rendered": "Hola Ana, te recordamos tu cita del viernes 19 a las 10:00 en Clínica Norte. Responde a este mensaje si necesitas cambiarla.",
"variableCount": 2,
"resolvedValues": { "1": "Ana", "2": "viernes 19 a las 10:00" },
"warnings": []
}
}

variables accepts an array (index 0 fills {{1}}) or an object keyed by position ({"1": "Ana"}), with up to 20 values of 400 characters each. With GET, pass them as the variables query parameter.

3

Submit for approval

curl -sS -X POST "https://api.jelliu.co/api/whatsapp-templates/$TEMPLATE_ID/submit" \
-H "Authorization: Bearer $JELLIU_API_KEY"

The template moves to pending. Only draft and rejected templates can be submitted or edited; anything else returns 400: Only draft or rejected templates can be edited. Submitted templates are managed by Meta.

4

Wait for approved

Poll GET /api/whatsapp-templates/{templateId} until status is approved. Meta’s review can take from minutes to days.

Test sends

POST /api/whatsapp-templates/{templateId}/test-send sends an approved template to any E.164 number so you can see it on a real phone, without creating a contact:

{ "toNumber": "+573001112233", "variables": ["Ana", "viernes 19 a las 10:00"] }

It returns 202 with messageSid, status, to, from and rendered.

A test send is a real, billed WhatsApp message: there is no simulation mode. Test sends are capped at 10 per workspace and 3 per template per UTC day, and they also count against the workspace’s daily WhatsApp cap. Units are refunded when the provider rejects the send.

Deleting templates

DELETE /api/whatsapp-templates/{templateId} removes the template at the provider and in Jelliu. A deleted template’s name stays reserved for that language: creating another template with the same name and language fails with 409.

Sending messages

Two endpoints send to a contact. Both take a contactId (the contact must have a WhatsApp number and belong to your workspace), both accept a write key, and both run the same checks.

EndpointYou chooseUse it when
POST /api/whatsapp/sendFreeform message or templateIdYou know which side of the 24-hour window the contact is on.
POST /api/whatsapp/reachOnly the textYou don’t know. Jelliu picks the form for you.

POST /api/whatsapp/send

contactId
string (uuid)Required

The contact to message.

message
string

Freeform text, 1 to 4096 characters. Allowed only inside the 24-hour window.

templateId
string (uuid)

An approved template. Works inside and outside the window. When both are set, the template wins.

templateVariables
string[] | object

Values for the template’s placeholders: an ordered array, or an object keyed by position such as {"1": "Ana"}.

Provide message or templateId. Neither returns 400: Provide either a message or a templateId.

curl -sS -X POST "https://api.jelliu.co/api/whatsapp/send" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contactId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"templateId": "3f1c9a52-7b0e-4d3a-9c21-5e8f7a6b4d10",
"templateVariables": ["Ana", "viernes 19 a las 10:00"]
}'
200 OK
{
"data": {
"messageSid": "SM2a4c6e8f0a1b3c5d7e9f1a2b3c4d5e6f",
"status": "queued"
}
}

POST /api/whatsapp/reach

contactId
string (uuid)Required

The contact to message.

message
stringRequired

1 to 4096 characters. Sent as written when the window is open.

language
stringDefaults to es

2 to 10 characters. The template language to fall back on when the window is closed.

  • Window open: message is sent as freeform text and data.form is freeform.
  • Window closed: your newest approved template in language is sent instead and data.form is template. Your text is not delivered; the template opens the conversation, and once the contact replies the window is open again.
  • Window closed and no approved template in that language: nothing is sent, and the call fails with 422 NO_TEMPLATE_AVAILABLE.
200 OK
{
"data": {
"messageSid": "SM9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d",
"status": "queued",
"form": "template",
"note": "WhatsApp only allows an approved template until the person writes to the line first, so an opening message was sent — NOT the material. Tell the person exactly that: you just wrote to them on WhatsApp, and if they reply there you will send the whole thing. Do not claim the information itself has been sent."
}
}

note is written for an AI agent to relay to the contact. Branch on form in your own code.

Checks before every send

Both endpoints run these checks in order. A failure stops the send before any quota is used.

  1. The contact exists in your workspace and has a WhatsApp number.
  2. Compliance. The number is checked against your compliance rules: allowed contact hours for the contact’s country, blocked prefixes, the suppression list, do-not-call status, and the daily and total attempt limits.
  3. Opt-out. A contact with status dnc or an opt-out date cannot be messaged.
  4. Message form. A template must exist, be approved and be ready at the provider; freeform requires the 24-hour window.
  5. Daily cap. One unit of the workspace’s daily WhatsApp cap is consumed. It is refunded if the send then fails.
  6. Sender. An ONLINE sender must exist.

Unlike email, WhatsApp sends respect the allowed contact hours of your compliance configuration. By default that is 08:00 to 20:00 in the contact’s country (for Colombia, America/Bogota). If your workspace has a timezone set, the hours are evaluated in that timezone instead. A send outside the window fails with 423 COMPLIANCE_BLOCKED, for example Outside allowed call hours (08:00-20:00 America/Bogota).

The attempt limits count calls to the contact. A contact who already received the maximum number of calls today (3 by default) cannot be messaged on WhatsApp until the next day either.

WhatsApp campaigns

A campaign with channel set to whatsapp writes first to every contact with the campaign’s approved template. Set whatsappTemplateId, whatsappTemplateVariables (literal values) and whatsappTemplateVariableMap (per-contact values such as {"1": "contact.first_name"}) on the campaign. Activation is refused if the template is not approved or a placeholder has no value source. Sends are paced at roughly one every 1.5 seconds per worker, and the last 20% of the daily cap (at least 25 messages) is reserved for replies so a campaign cannot leave your agent unable to answer. See Campaigns.

Inbound messages

When a contact writes to one of your numbers, Jelliu:

  1. Routes by the receiving number. The number must belong to exactly one workspace with an ONLINE sender or an active phone number. Messages to numbers that are unknown, or claimed by more than one workspace, are dropped.
  2. Checks for an opt-out before anything else (see below).
  3. Finds the contact. It matches the sender’s number against contacts’ WhatsApp and phone numbers, preferring a contact in an active campaign, then a paused one, then a completed one. People who are not in any campaign are welcome: Jelliu creates a contact for them, unless the number is on the suppression list.
  4. Picks the agent. The campaign’s agent if it serves WhatsApp; otherwise the agent bound to the receiving number; otherwise the workspace’s oldest agent that serves WhatsApp.
  5. Opens the 24-hour window for that contact.
  6. Generates and sends the reply in the background. If the agent has a response delay or human pacing configured, the reply waits, and several messages sent in a row are answered together.

Voice notes are transcribed and answered; the reply to a voice note includes a spoken version when it is short enough (600 characters or less), and the text is always sent too. Photos and PDFs sent with or without a caption reach the agent together with the caption.

Inbound conversations and their messages are available through Conversations.

AI replies on WhatsApp draw from your plan’s monthly allowance of AI messages. When it is used up, the agent stops replying and the workspace receives an in-app notification. See Limits.

Opt-out

A message is treated as an opt-out when the whole message is one of these keywords (case, extra spaces and trailing punctuation are ignored):

stop, stopall, stop all, cancel, unsubscribe, quit, end, baja, cancelar, darme de baja, dar de baja, no molestar

Longer phrasings such as “no me vuelvas a escribir” are also detected. Keywords inside a sentence are not: “quiero cancelar mi cita” is a request to reschedule, and the agent answers it. Voice notes are checked after transcription.

When an opt-out is detected, Jelliu:

  • sets every contact in your workspace with that number to dnc and records the date and the matched keyword;
  • adds the number to the workspace’s suppression list, which survives deleting and re-importing the contact;
  • sends nothing back, not even a confirmation.

From then on, every send to that number fails with 403 CONTACT_OPTED_OUT or 423 COMPLIANCE_BLOCKED, and inbound messages from it are not answered. See Compliance to review or lift a suppression.

Delivery statuses

Jelliu subscribes to the provider’s delivery callbacks for every message it sends and records the latest status per message. Statuses only move forward: a late failed never overwrites a message already recorded as delivered or read.

StatusMeaning
queued, accepted, scheduledAccepted for sending.
sending, sentHanded to WhatsApp.
deliveredDelivered to the device.
readRead by the contact.
failed, undeliveredCould not be delivered.

The status in a send response is the status at the moment of sending, usually queued.

Per-message delivery statuses are not currently exposed through the API or outbound webhooks.

Errors

Send errors from /api/whatsapp/send and /api/whatsapp/reach use the standard envelope with a WhatsApp-specific code:

{
"error": {
"code": "OUTSIDE_24H_WINDOW",
"message": "Outside the 24-hour window: this contact has not messaged you in the last 24h, so Meta requires an approved template to reach them. Pick a template to send."
}
}
StatusCodeCause and fix
400VALIDATION_FAILEDThe body failed validation. See details.
400MESSAGE_OR_TEMPLATE_REQUIREDNeither a non-empty message nor a templateId was sent.
400NO_WHATSAPP_NUMBERContact has no WhatsApp number. Add one to the contact.
400INVALID_RECIPIENTThe contact’s number is not a valid WhatsApp number.
403CONTACT_OPTED_OUTContact has opted out — cannot send WhatsApp message.
404TEMPLATE_NOT_FOUNDTemplate not found in this workspace.
409SENDER_NOT_REGISTEREDYour sender exists but is still being approved or is offline. The message names the number and its status.
422SENDER_NOT_REGISTEREDNo WhatsApp sender is set up for this account yet. Register your number in Settings -> WhatsApp before sending.
422OUTSIDE_24H_WINDOWFreeform text outside the window. Send an approved template, or use /reach.
422TEMPLATE_NOT_APPROVEDThe template is not approved yet. The message includes its current status.
422NO_TEMPLATE_AVAILABLE/reach only: the window is closed and there is no approved template in language.
423COMPLIANCE_BLOCKEDBlocked by compliance: outside allowed hours, suppressed, do-not-call, blocked prefix or attempt limit. The message says which.
429DAILY_CAP_REACHEDDaily WhatsApp send cap reached for this tenant (1000/day). Try again tomorrow or contact support to raise the limit.
429RATE_LIMITEDThrottled by WhatsApp. Retry shortly.
429RATE_LIMIT_EXCEEDEDJelliu’s per-minute rate limit. Honor Retry-After.
502WHATSAPP_SEND_FAILEDAny other provider error.

Sender and template endpoints return the generic codes from Errors: VALIDATION_FAILED (400, or 409 for duplicates such as +573001234567 is already registered as a WhatsApp sender.), FORBIDDEN, NOT_FOUND and RATE_LIMIT_EXCEEDED. A test send over its daily cap returns 429 RATE_LIMIT_EXCEEDED with a Spanish message such as Ya enviaste 3 pruebas de esta plantilla hoy. Cada prueba es un mensaje real que WhatsApp factura; intentalo manana.

Limits

LimitValue
Requests to /api/whatsapp/*20 per minute per workspace, and fails closed. See Rate limits.
POST /api/whatsapp/send, /reach and sender and template mutationsAlso count against the shared 10 per minute budget for configuration mutations.
Daily WhatsApp sends1,000 per workspace per UTC day by default; 25,000 on plans with unlimited monthly AI messages. Includes agent replies, API sends, campaign sends and test sends.
Reply reserveCampaign sends stop at 80% of the daily cap (keeping at least 25 messages for replies).
Template test sends10 per workspace and 3 per template per UTC day.
Freeform message length4,096 characters.
Template body1,024 characters, up to 20 variables.

Monthly AI message allowance

AI replies on WhatsApp, email, web chat, Instagram and Messenger share one monthly allowance per plan:

PlanAI messages per month
Starter1,200
Growth4,000
Business9,000
EnterpriseUnlimited

When the allowance is used up, or the workspace has no active plan, AI replies stop with 403 BILLING_ERROR, for example Llegaste al límite mensual de mensajes de IA (1200 mensajes del plan starter). Sube de plan en Ajustes → Plan. Messages you send yourself through /api/whatsapp/send are limited by the daily cap, not by this allowance. See Billing and usage.

Scopes at a glance

OperationMinimum key scope
Read senders and templates, preview with GETread
Send and reach contacts, preview with POSTwrite
Register, verify, update or delete senders; create, submit, test or delete templatesfull