Messaging API
Send an individual message, manage the WhatsApp templates you send from, and read what people send back.
This is not the campaign API. A campaign targets an audience and is measured as a campaign; these routes send one message to one person, for the cases an audience does not describe - an order update, a support reply, a confirmation your own system decided to send.
All endpoints on this page are relative to the base URL:
https://api-eu1.joryio.com/api - see API Overview.
Authentication
Authorization: Bearer your_api_key_or_jwt
Content-Type: application/json
Sending needs whatsapp:write; reading templates, usage and inbound messages
needs the matching read scope.
Sending WhatsApp
Send a Template Message
POST /whatsapp/send/template
The only thing you can send to someone outside an open 24-hour conversation window. The template must already be approved by Meta.
{
"to": "+306912345678",
"templateName": "order_shipped",
"language": "en",
"variables": { "1": "Alex", "2": "GR-40921" }
}
Response:
{ "messageId": "wamid.HBgM…", "status": "queued" }
Send a Session Message
POST /whatsapp/send/text
Free-form text, allowed only while a 24-hour window is open - that is,
within 24 hours of that person's last inbound message. Outside it this returns
409 and you must send a template instead.
{ "to": "+306912345678", "text": "Your order is out for delivery." }
WhatsApp does not let you start a conversation with free text. If you are not
sure a window is open, send a template - a 409 after a failed send is a
message the customer never received.
Templates
List Templates
GET /whatsapp/templates
Every template with its approval status. Only APPROVED templates can be sent.
Response:
{
"templates": [
{ "name": "order_shipped", "language": "en", "status": "APPROVED",
"category": "UTILITY", "variables": 2 },
{ "name": "winback_2026", "language": "en", "status": "PENDING",
"category": "MARKETING", "variables": 1 }
]
}
Create a Template
POST /whatsapp/templates
Submits a template to Meta for approval. Approval is not immediate and can be
rejected; poll GET /whatsapp/templates for the outcome.
{
"name": "order_shipped",
"language": "en",
"category": "UTILITY",
"body": "Hi {{1}}, order {{2}} is on its way.",
"variables": 2
}
SMS templates (Delivyr only)
Delivyr only. These routes work only for a workspace whose SMS number is linked to a Delivyr account. Delivyr sends only templates it approved, to US and Canada numbers. Every route except GET /sms/templates/accounts answers 403 with code: "delivyr_account_required" for any other workspace:
{ "statusCode": 403, "code": "delivyr_account_required",
"message": "SMS templates are available only to workspaces that send SMS through a Delivyr account. Connect one under Integrations > Delivyr." }
Requires templates:read to read and templates:write to create, refresh or delete.
List the workspace's Delivyr accounts
Returns [] when the workspace has none, so a client can check before calling anything else. key is the accountKey the other routes take.
GET /sms/templates/accounts
[
{ "key": "6f1c2a9e-3b4d-4e5f-8a7b-9c0d1e2f3a4b", "smsAccountId": "6f1c2a9e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
"provider": "delivyr", "name": "Delivyr US" }
]
List templates
Optional filters: status (pending, approved, rejected, system) and accountKey. Only approved and system templates can be sent. mappableVariables are the placeholders you fill per contact; Delivyr fills the others itself (account_dba).
GET /sms/templates?status=approved
[
{ "id": "1d2e3f40-5a6b-4c7d-8e9f-0a1b2c3d4e5f", "accountKey": "6f1c2a9e-3b4d-4e5f-8a7b-9c0d1e2f3a4b",
"provider": "delivyr", "providerTemplateId": "NMPNWAS6", "title": "We Miss You",
"body": "Hi {{first_name}}, we miss you: {{link}}", "status": "approved", "source": "portal",
"variables": ["first_name", "link"], "mappableVariables": ["first_name", "link"] }
]
Refresh from Delivyr
Pulls the account's templates from Delivyr now, including ones made in the Delivyr portal, and updates their review status. Joryio also refreshes every 30 minutes. Optional body: { "accountKey": "..." }.
POST /sms/templates/sync
{ "results": [ { "accountKey": "6f1c2a9e-3b4d-4e5f-8a7b-9c0d1e2f3a4b", "added": 2, "updated": 5, "removed": 0, "total": 7 } ] }
Create a template
Submits the template to Delivyr's review. It starts as pending, and the status changes after Delivyr reviews it. Use {{name}} placeholders. Put {{link}} where a link goes, and Joryio fills it with its own tracked short link when you send. accountKey is needed only when the workspace sends through more than one Delivyr account. Approved text cannot be changed. To change the wording, create a new template.
POST /sms/templates
{
"title": "Weekend offer",
"body": "Hi {{first_name}}, your weekend offer is ready: {{link}} Reply STOP to opt out.",
"tags": ["promo"]
}
Get or delete a template
Deleting also deletes the template in Delivyr. Delivyr's own system templates cannot be deleted.
GET /sms/templates/:id
DELETE /sms/templates/:id
Usage and limits
Conversation Usage
GET /whatsapp/usage?period=monthly&startDate=2026-08-01&endDate=2026-08-31
WhatsApp bills per 24-hour conversation, not per message, so message counts do not predict cost. This returns the stored usage rows for the workspace's accounts - one row per account and period, newest period first.
| Parameter | Description |
|---|---|
wabaId | Optional. Only this WhatsApp Business Account. |
period | Optional. daily or monthly. |
startDate / endDate | Optional. ISO dates; rows whose period starts on or after startDate and ends on or before endDate. |
Response - a JSON array:
[
{
"id": "usage_9f2c", "wabaId": "waba_120363", "period": "monthly",
"periodStart": "2026-08-01", "periodEnd": "2026-08-31",
"conversations": { "marketing": 4120, "utility": 8802, "authentication": 310, "service": 1904, "total": 15136 },
"messages": { "sent": 31204, "delivered": 30877, "read": 22410, "failed": 327, "received": 4810 },
"estimatedCost": 812.4,
"currency": "USD"
}
]
Each row also carries organizationId, workspaceId, createdAt and updatedAt.
Messaging Limits
GET /whatsapp/usage/limits?wabaId=waba_120363
The tier Meta currently allows for your number, and how much of it you have used. Hitting the ceiling fails sends - it does not queue them.
Response:
{
"tier": "TIER_10K",
"dailyLimit": 10000,
"dailyConversations": 3410,
"monthlyConversations": 41200,
"monthlyEstimatedCost": 812.4,
"qualityRating": "GREEN"
}
wabaId is optional - without it the limits are the default account's, and the
three usage figures read 0, because usage is only looked up for a named account.
A contact's messages
GET /messages?userId=665f1e2a9b3c4d5e6f7a8b9c&direction=outbound&channel=email&limit=50&offset=0
What you sent a contact and what it sent you, newest first: every
message.sent from a campaign or a journey (email, SMS, WhatsApp, push, Viber -
not webhook nodes) and the replies GET /messages/inbound returns. This is the
contact profile's Messages tab.
| Parameter | Description |
|---|---|
userId | Required. The contact's Joryio id or its externalId. An unknown contact answers an empty page. |
direction | Optional. outbound (sends) or inbound (replies); omit for both. |
channel | Optional. One channel, e.g. email, sms, whatsapp, push. |
limit | Optional. Default 50, clamped to 1-200. |
offset | Optional. Rows to skip, default 0, at most 5000. |
Response:
{
"data": [
{ "direction": "outbound", "channel": "email", "eventName": "message.sent",
"timestamp": "2026-10-28 10:00:00.000", "subject": "Your cart is still here",
"messageId": "0100019a2b3c", "status": "opened",
"source": { "kind": "journey", "id": "8f0c2d1e-5b7a-4c3e-9f1a-2d3e4f5a6b7c", "name": "Cart recovery" },
"body": null, "from": null, "toNumber": null, "account": null, "messageType": null },
{ "direction": "inbound", "channel": "sms", "eventName": "sms.message.received",
"timestamp": "2026-10-27 18:02:00.000", "body": "thanks!", "from": "+306912345678",
"toNumber": "+302101234567", "account": "Support line", "messageType": "text",
"subject": null, "messageId": null, "source": null, "status": null }
],
"total": 2
}
A send names what sent it in source (kind is journey or campaign,
name is null when it can no longer be resolved), the email subject, and
status - the furthest outcome recorded for that message: sent, delivered,
opened, clicked, or failed / bounced, which win over the others. A reply
carries from, toNumber, account and body as on /messages/inbound.
The route reads the last 12 months; total is the size of the page, so page
with offset until a page comes back shorter than limit.
Inbound messages
Read Replies
GET /messages/inbound?userId=665f1e2a9b3c4d5e6f7a8b9c&channel=whatsapp&limit=50&offset=0
What one contact has sent to you, across WhatsApp and SMS - the rows come
from the contact's <channel>.message.received events. Poll this if you route
replies into a helpdesk or your own automation.
| Parameter | Description |
|---|---|
userId | Required. The contact's Joryio id (the id the Users API returns). Without it the route answers an empty page. |
channel | Optional. One channel only: sms or whatsapp. |
limit | Optional. Default 50, clamped to 1-200. |
offset | Optional. Rows to skip, default 0. |
Response:
{
"data": [
{ "channel": "whatsapp", "from": "+306912345678", "toNumber": "+302101234567",
"account": "Support line", "body": "when will it arrive?", "messageType": "text",
"eventName": "whatsapp.message.received", "timestamp": "2026-08-27T09:41:00Z" }
],
"total": 1
}
total is the size of the page returned, not a count of everything the contact
ever sent - page with offset until a page comes back shorter than limit.
There is no since cursor on this route. toNumber is the number the message
arrived on and account the receiving account's label; both are null for
messages recorded before they were stamped.
SMS Message History
GET /sms/messages?userId=665f1e2a9b3c4d5e6f7a8b9c&limit=50
A contact's SMS messages (the profile timeline), newest first. userId is
required - without it the route answers []. limit defaults to 50 and is
clamped to 1-200.
Response - a JSON array:
[
{ "id": "sms_411", "body": "STOP", "phoneNumber": "+306912345678",
"direction": "inbound", "messageType": "text", "accountId": "acct_7a1",
"createdAt": "2026-08-26T18:02:00Z" }
]
direction is inbound or outbound. There is no carrier delivery status on
this route - delivery and failure are events on the contact's timeline
(GET /users/:id/events).
Error Responses
| Status | Meaning |
|---|---|
| 400 | Malformed request or unknown template |
| 401 | Missing or invalid credentials |
| 403 | The key lacks the required scope |
| 409 | No open 24-hour window - send a template instead |
| 429 | Messaging limit reached for the day |
| 500 | Internal server error |