REST API reference for Relay - By CEQUENS: send SMS and WhatsApp messages, manage contacts, segments and campaigns, and read delivery logs. 58 endpoints, authenticated with a single API key. This page is public — no account needed to read it.
Two hosts, one API key
Generate a REST API key in the portal, then send it as
Authorization: Bearer <key> or X-API-Key. One key works
for both hosts. Which host serves a request is decided entirely by its path:
GW
Messaging — send SMS and WhatsApp, read and write contact tags and
CDP attributes. Every path under /api/v1/. https://ops.relay.cequens.com
CP
Management API — contacts, segments, campaigns, message logs, apps
and billing reads. Every path under /api/mgmt/v1/. A key must have
Management API access enabled, which is a per-key setting in the portal. https://portal.relay.cequens.com
The messaging host sends no CORS headers, so those endpoints are server-to-server
only — they cannot be called from a browser.
Platform
Platform connectivity check — no authentication required, useful for uptime monitoring. Every other endpoint uses API key auth (Authorization: Bearer <key> or X-API-Key: <key>).
GET/api/healthMessaging host
Platform health check
Returns the connectivity status of MongoDB and NATS. No authentication required — useful for uptime monitoring.
cURL
curl -X GET "https://ops.relay.cequens.com/api/health" \
-H "Content-Type: application/json"
JavaScript
const res = await fetch("https://ops.relay.cequens.com/api/health", {
method: "GET",
headers: {
"Content-Type": "application/json"
},
});
const data = await res.json();
console.log(data);
CRM for managing subscribers — create, query, import and delete contacts. Also includes the gateway API Key endpoints for reading and updating contacts and CDP attributes programmatically; those run on the messaging host, the rest on the management host. Each endpoint below names the host that serves it.
GET/api/mgmt/v1/contactsManagement hostAPI key
List contacts
Paginated contact list. Supports filtering by status, segment, tag, and full-text search.
Upsert array of contacts. Existing phones are skipped (no update). Returns imported vs skipped counts. Optional addToSegment puts every contact in the file (new and already-existing) into a fixed segment: { segmentId } for an existing fixed segment, or { name } to create one. The choice is validated before any contact is written; if adding to the segment fails afterwards, the import still stands and segmentError explains.
Returns tags and CDP attributes for a phone number via API key. Returns 404 if the contact has never been seen by your account. Served by the SMS gateway host, not the portal host (see the base URL in the examples below).
Sets or updates CDP attributes. Existing keys not in the payload are preserved. Max 50 key-value pairs per request. Values are stored as strings. Served by the SMS gateway host, not the portal host.
Create a segment. With a query (and/or status, optInStatus, tag, segment filters) and membership: "live" (the default) the rule is stored and re-evaluated on every read (type: "dynamic"). With membership: "fixed" the contacts matching right now are saved as a static segment (up to 50,000; truncated reports a cut-off), and/or the hand-picked contactIds (max 5,000) are added. With neither a rule nor ids an empty fixed segment is created.
Send SMS via the gateway API key, and query, filter, or export the delivery log from the portal. The two sit on different hosts — check the base URL on each endpoint.
GET/api/mgmt/v1/sendersManagement hostAPI key
Get available sender IDs
Returns all approved sender IDs for the organisation.
Submits an outbound SMS through the gateway routing pipeline. Returns 202 Accepted when queued. Pass the API key as Authorization: Bearer <key> or X-API-Key: <key>. Served by the SMS gateway host, not the portal host (see the base URL in the examples below).
Request body
from — registered Sender ID. to — E.164 destination. campaignId — optional, links the log to a campaign.
Send to up to 1,000 recipients in one call. Use {variable} placeholders in text for per-recipient personalization. The entire request counts as 1 TPS unit. App limits are enforced automatically from the API key — no appId needed in the body. If quota covers only part of the batch, the first N are queued and the rest returned as quota_exhausted rejections. DND and content-filter checks happen downstream at dispatch time. Served by the SMS gateway host, not the portal host (see the base URL in the examples below).
Request body
text supports {variable} placeholders (case-insensitive replacement). Recipients without variables receive the raw template (broadcast mode). App quota is resolved from the API key automatically. Max 1,000 recipients per call.
curl -X POST "https://ops.relay.cequens.com/api/v1/sms/messages/bulk" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"from":"BRAND","text":"Hi {name}, your order {orderId} is ready for pickup!","recipients":[{"to":"+12025551001","variables":{"name":"Alice","orderId":"A-123"}},{"to":"+12025551002","variables":{"name":"Bob","orderId":"B-456"}},{"to":"+12025551003"}],"campaignId":"64f1..."}'
{
"note": "Returns text/csv — use in a download link or curl -o export.csv"
}
WhatsApp
Send WhatsApp messages via your registered numbers. Supports template, text, and media (image / document / audio / video / sticker). Same API-key auth as SMS, and like SMS these routes run on the SMS gateway host, not the portal host.
Submits an outbound template message through Meta Cloud API. The only message type allowed when the contact has no open 24h service window. Served by the SMS gateway host, not the portal host (see the base URL in the examples below). Pass API key as Authorization: Bearer <key> or X-API-Key: <key>. Idempotency-Key header (optional): identical key from the same account returns the previously-queued messageId without re-sending.
Request body
from — your registered WhatsApp number (E.164 or phoneNumberId). to — E.164 destination. template.name — Meta-approved template name. template.language — locale code (must be APPROVED for that template). template.variables — positional map matching {{1}}, {{2}}, … placeholders in the template body. For advanced cases pass template.components (Meta-shape) directly instead of variables.
Free-form text. Only allowed when the recipient has messaged you within the last 24 hours (open service window). Outside the window, use a template send.
Request body
text.body — message text (max 4096 chars). text.preview_url — set true to render link previews. replyToWamid — optional: thread this message as a reply to a previous inbound.
{
"from": "+31644102243",
"to": "+12025551234",
"type": "text",
"text": {
"body": "Thanks for reaching out — your order will ship today.",
"preview_url": false
},
"replyToWamid": "wamid.HBgL…"
}
cURL
curl -X POST "https://ops.relay.cequens.com/api/v1/whatsapp/messages" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"from":"+31644102243","to":"+12025551234","type":"text","text":{"body":"Thanks for reaching out — your order will ship today.","preview_url":false},"replyToWamid":"wamid.HBgL…"}'
JavaScript
const res = await fetch("https://ops.relay.cequens.com/api/v1/whatsapp/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer <api_key>"
},
body: JSON.stringify({
"from": "+31644102243",
"to": "+12025551234",
"type": "text",
"text": {
"body": "Thanks for reaching out — your order will ship today.",
"preview_url": false
},
"replyToWamid": "wamid.HBgL…"
}),
});
const data = await res.json();
console.log(data);
Responses
202
Queued
{
"messageId": "wa_…",
"status": "queued"
}
409
Service window closed
{
"error": "No open 24h service window for this contact — use a template send instead",
"code": "service_window_closed"
}
Send image, document, audio, video, or sticker. Same service-window rules as text. Provide either link (public HTTPS URL Meta fetches) or id (a Meta media id from a prior upload).
Request body
For document add filename. For audio and sticker, captions are not supported. Replace the image key with document / audio / video / sticker to match the chosen type.
{
"from": "+31644102243",
"to": "+12025551234",
"type": "image",
"image": {
"link": "https://cdn.example.com/orders/abc.jpg",
"caption": "Your order is on its way!"
}
}
cURL
curl -X POST "https://ops.relay.cequens.com/api/v1/whatsapp/messages" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"from":"+31644102243","to":"+12025551234","type":"image","image":{"link":"https://cdn.example.com/orders/abc.jpg","caption":"Your order is on its way!"}}'
JavaScript
const res = await fetch("https://ops.relay.cequens.com/api/v1/whatsapp/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer <api_key>"
},
body: JSON.stringify({
"from": "+31644102243",
"to": "+12025551234",
"type": "image",
"image": {
"link": "https://cdn.example.com/orders/abc.jpg",
"caption": "Your order is on its way!"
}
}),
});
const data = await res.json();
console.log(data);
Responses
202
Queued
{
"messageId": "wa_…",
"status": "queued"
}
400
Media reference missing
{
"error": "image requires either `image.link` (public HTTPS URL) or `image.id` (Meta media id)"
}
Returns the current status and Meta wamid for a previously-submitted message. Served by the SMS gateway host, not the portal host (see the base URL in the examples below).
Parameters
Name
In
Type
Description
messageId *
path
string
The messageId returned by POST /api/v1/whatsapp/messages wa_lxky9z_a1b2c3d4
Send email through your verified sender identities (a Relay address or your own DKIM-verified domain). Same API-key auth as SMS and WhatsApp, and like them these routes run on the SMS gateway host, not the portal host. Delivery, bounce, complaint, open and click events update the message record and fire your webhooks.
POST/api/v1/email/messagesMessaging hostAPI key
Send using a template
Queues an email built from one of your approved email templates. Tokens in the subject and body are filled from variables plus the recipient contact. The unsubscribe footer and List-Unsubscribe headers are added automatically. Idempotency-Key header (optional): the same key from this account returns the previously-queued messageId without re-sending.
Request body
to — recipient address. from — either a full address string ("Brand <hello@yourdomain.com>") or { identityId, address?, name? }; omit it to use your default sender. templateId — an approved EmailTemplate. locale — optional language variant. variables — token values merged into the subject and body. Optional: if the address matches a contact in your account, its name, first_name, phone, country and CDP attributes fill in automatically, and anything you pass in variables overrides them. replyTo — optional override.
{
"error": "Missing API key (Authorization: Bearer <key> or X-API-Key header)"
}
403
From address not allowed for that sender
{
"error": "From address news@other.com is not allowed for this sender",
"code": "from_not_allowed"
}
404
No matching sender identity
{
"error": "No sender identity matches `from`. Set one up under Channels → Email → Sender setup.",
"code": "sender_not_found"
}
409
Sender domain not verified
{
"error": "Sender domain mail.example.com is not verified",
"code": "sender_not_verified"
}
422
Suppressed recipient or unapproved template
{
"error": "Recipient is on the email suppression list",
"code": "suppressed",
"reason": "bounce"
}
429
TPS cap reached (per channel)
{
"error": "TPS cap of 50 msg/s reached",
"code": "throughput_exceeded"
}
503
Email not configured on this deployment
{
"error": "Email sending is not configured on this deployment",
"code": "email_not_configured"
}
POST/api/v1/email/messagesMessaging hostAPI key
Send raw HTML
Queues an email from HTML you supply. Requires a verified own domain — raw sends are not allowed from a shared Relay address. The HTML is sanitised (scripts, iframes and forms are stripped) and an unsubscribe footer is appended when {unsubscribe_url} is absent.
Request body
subject is required, plus at least one of html / text. Total body size must stay under 9 MB. Set marketing: true to have the send treated as promotional (quiet hours, frequency caps and the CRM unsubscribe then apply).
{
"from": "Brand <hello@mail.yourcompany.com>",
"to": "omar@example.com",
"subject": "Your order SA-10233 has shipped",
"html": "<p>Hi Omar, your order is on its way.</p><p><a href=\"{unsubscribe_url}\">Unsubscribe</a></p>",
"text": "Hi Omar, your order is on its way."
}
cURL
curl -X POST "https://ops.relay.cequens.com/api/v1/email/messages" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{"from":"Brand <hello@mail.yourcompany.com>","to":"omar@example.com","subject":"Your order SA-10233 has shipped","html":"<p>Hi Omar, your order is on its way.</p><p><a href=\"{unsubscribe_url}\">Unsubscribe</a></p>","text":"Hi Omar, your order is on its way."}'
JavaScript
const res = await fetch("https://ops.relay.cequens.com/api/v1/email/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer <api_key>"
},
body: JSON.stringify({
"from": "Brand <hello@mail.yourcompany.com>",
"to": "omar@example.com",
"subject": "Your order SA-10233 has shipped",
"html": "<p>Hi Omar, your order is on its way.</p><p><a href=\"{unsubscribe_url}\">Unsubscribe</a></p>",
"text": "Hi Omar, your order is on its way."
}),
});
const data = await res.json();
console.log(data);
Returns the delivery state of a previously-submitted email, including bounce reason, open and click counts. Served by the SMS gateway host, not the portal host.
Parameters
Name
In
Type
Description
messageId *
path
string
The messageId returned by POST /api/v1/email/messages em_lxky9z_a1b2c3d4
Phone a contact from your own systems. On the answered call Relay can speak a message (e.g. a verification code or an appointment reminder), run one of your call flows (a menu such as "press 1 to confirm"), or let a Voice AI agent hold the conversation. Calls are queued, placed from one of your numbers, and billed per minute in the increments shown in your plan; unanswered calls are free. Outgoing calls must be enabled for your account by Relay.
POST/api/v1/voice/callsMessaging hostAPI key
Place a voice call
Queues an outgoing call. Provide exactly one of message (text Relay speaks, up to 1000 characters — repeat 1–3 times, optional pressToRepeat for "press 1 to hear it again"), flowId (a published call flow with *Outbound calls* turned on) or agentId (an active Voice AI agent). to is the destination in international format. from is optional: one of your numbers that can place calls; otherwise the flow's or agent's number is used (for a message, your first calling number). variables (flat key/value pairs) are spoken in flow prompts as {name} or given to the agent as context. Set transactional: true for calls the person asked for, such as a verification code — they are placed outside quiet hours and ignore the marketing opt-out list. Send an Idempotency-Key header (or clientRef) to make retries safe. Message text is never stored after the call. Pass the API key as Authorization: Bearer <key> or X-API-Key: <key>. Served by the SMS gateway host, not the portal host.
Request body
Spoken message (above), or { "to": "+9665…", "flowId": "66f1…", "variables": { "appointment": "Tuesday 4 pm" } } for a call flow, or { "to": "+9665…", "agentId": "66f1…", "variables": { "firstName": "Sara" } } for a Voice AI agent.
{
"error": "Provide exactly one of `message`, `flowId` or `agentId`",
"code": "invalid_request"
}
401
Missing or invalid API key
{
"error": "Missing API key"
}
402
Balance too low for a call
{
"error": "Your balance does not cover a call — top up your wallet",
"code": "insufficient_funds"
}
403
Outgoing calls not enabled, destination not allowed, or from not yours
{
"error": "Outbound calling is not enabled for your account — contact support to enable it",
"code": "outbound_not_enabled"
}
409
Flow not published / not outbound, agent inactive, quiet hours, opted-out contact, or no calling number
{
"error": "This call flow is not enabled for outbound calls — turn on \"Outbound calls\" in the flow",
"code": "flow_not_outbound"
}
429
Too many calls per second, or the project's monthly call limit
{
"error": "Too many calls started in the last second — try again shortly",
"code": "rate_limited"
}
GET/api/v1/voice/calls/:idMessaging hostAPI key
Get call status
The state of a call you placed: queued → ringing → answered → completed (or no_answer, busy, rejected, failed), with the end reason, talk time, billed seconds and charge. Agent calls include the transcript; call-flow calls include the menu choices. Served by the SMS gateway host, not the portal host.
Parameters
Name
In
Type
Description
id *
path
string
The callId returned by POST /api/v1/voice/calls 66f1c0ffee0000000000beef
The portal's own copy of Place a voice call — same body (message, flowId or agentId, plus to, from, variables, transactional) and responses. For your own systems, prefer POST /api/v1/voice/calls on the gateway host with an API key. Requires admin or editor role.
One call with its full transcript, the tools the agent used, the end reason, the billed duration and charge, and the handover case id when the agent passed the caller to your team.
{
"_id": "66f1...",
"status": "completed",
"endReason": "agent_hangup",
"durationSec": 75,
"billableSec": 120,
"transcript": [
{
"role": "agent",
"text": "Hello Sara, this is Relay Clinic calling about your appointment."
},
{
"role": "caller",
"text": "Yes, go ahead."
}
],
"caseId": null
}
404
Not found
{
"error": "Call not found"
}
Automations
Start an automation from your own systems. An automation with the API trigger is started with an API key; one with the Inbound webhook trigger is started by a signed POST to its own URL. Both identify the person by phone number (a new number is added to your contacts), and the data you send becomes fields your steps can use as {field} — in message templates, If/else and Switch conditions. In the builder, the trigger panel shows the ready-to-copy call for each automation and lets you list required fields: a call missing any of them is refused before anything is created.
POST/api/webhooks/inbound/:slugManagement host
Inbound webhook (signed)
The URL of an automation with the Inbound webhook trigger. Create the endpoint in the automation builder (trigger panel → New endpoint): you choose the slug and where the phone number sits in your JSON, and receive the endpoint's secret once (rotate it there at any time).
Signing (required): compute an HMAC of the exact raw request body — the bytes you send, not a re-serialised copy — with the endpoint secret, using SHA-256 (or SHA-512 if the endpoint is set to it), hex-encode it and send it in the X-Signature-256 header (or the header configured on the endpoint). A sha256= prefix is accepted. Example in bash: SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.*= //'); in Node.js: crypto.createHmac('sha256', secret).update(body).digest('hex').
Who: the phone number is read from the endpoint's contact path — $.phone by default; dotted paths and list positions such as $.customer.phone or $.items[0].mobile are supported. A number not yet in your contacts is added.
Your data: the whole JSON body is available to steps — each top-level key as {key}, a nested object as {parent_child} (up to three levels), a list as {key_count}. A body key never replaces a contact field of the same name.
Required fields: if the automation lists required fields, a body where any is missing, null or blank (a.b for nested keys) is refused with 400 missing_fields after the signature check, before any contact is created. Not applied when the call resumes a run that is waiting for this webhook.
Waiting runs: if the person has a run of this automation paused on a *Wait for event → webhook callback* step, the call resumes that run (the body is recorded on the run) instead of starting a new one.
Retries: there is no idempotency key — each accepted call starts a new run.
Parameters
Name
In
Type
Description
slug *
path
string
The endpoint slug chosen in the builder shop-order-paid
Request body
Any JSON object. Send it with Content-Type: application/json and the signature header — the cURL example below omits the signature. Steps can then use {order_id}, {amount} and {customer_tier}.
Body is not JSON, no phone at the contact path, or an invalid phone number
{
"error": "Could not resolve contact identifier from path \"$.phone\""
}
401
Signature header missing, or the signature does not match
{
"error": "Invalid signature"
}
403
Contact limit of your plan reached (new phone), or the workspace is read-only
{
"error": "Workspace is read-only (subscription past due) — automations are paused"
}
404
Unknown slug, or the endpoint is disabled
{
"error": "Webhook endpoint not found"
}
409
Automation not active, emergency stop engaged, or a waiting run could not be resumed
{
"error": "Automation is paused — cannot enroll"
}
429
Concurrent-run limit or per-contact frequency cap reached
{
"error": "Frequency cap exceeded for this contact"
}
Projects
Usage containers — each project has its own monthly sending limit per channel (SMS counted in message parts, WhatsApp and email in outbound messages) plus the credentials assigned to it. Note that the routes keep the original /apps spelling: the resource was renamed to Project in the product, and the paths are deliberately left unchanged so existing integrations keep working.
Creates a new project. monthlyLimits caps how much this project may send each calendar month, per channel — SMS counts message parts, WhatsApp and email count outbound messages, voice counts outgoing calls placed through the voice API. Omit a channel or set it to 0 for no limit. Requires admin role.
Updates project configuration. monthlyLimits is replaced wholesale — send every channel (sms, whatsapp, email, voice). Limits cannot be set on the Default project, which stays uncapped because it catches every campaign and unassigned credential. The isDefault flag cannot be changed via this endpoint.
Returns time-series SMS delivery counts for the project (sent / delivered / failed / blocked / parts). For consumption against the monthly limits — including WhatsApp and email — use GET /api/apps/:id/usage.
Consumption so far this calendar month, per channel, read from the same counters the limits are enforced against. Units: SMS in message parts, WhatsApp and email in outbound messages. A limit of 0 means that channel is uncapped. metered: false means usage tracking is unavailable on this deployment, in which case limits are not being enforced.