Skip to main content

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." }
The window is the whole model

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.

ParameterDescription
wabaIdOptional. Only this WhatsApp Business Account.
periodOptional. daily or monthly.
startDate / endDateOptional. 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.

ParameterDescription
userIdRequired. The contact's Joryio id or its externalId. An unknown contact answers an empty page.
directionOptional. outbound (sends) or inbound (replies); omit for both.
channelOptional. One channel, e.g. email, sms, whatsapp, push.
limitOptional. Default 50, clamped to 1-200.
offsetOptional. 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.

ParameterDescription
userIdRequired. The contact's Joryio id (the id the Users API returns). Without it the route answers an empty page.
channelOptional. One channel only: sms or whatsapp.
limitOptional. Default 50, clamped to 1-200.
offsetOptional. 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​

StatusMeaning
400Malformed request or unknown template
401Missing or invalid credentials
403The key lacks the required scope
409No open 24-hour window - send a template instead
429Messaging limit reached for the day
500Internal server error