Relay API Reference

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/health Messaging 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);

Responses

200

Service statuses

{
  "status": "ok",
  "db": "connected",
  "nats": "connected"
}

Contacts

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/contacts Management host API key

List contacts

Paginated contact list. Supports filtering by status, segment, tag, and full-text search.

Parameters

NameInTypeDescription
page query integer Page number (default 1) 1
limit query integer Items per page, max 200 (default 50) 50
status query string subscribed or unsubscribed subscribed
tag query string Filter by tag vip
segment query string Segment ObjectId 64f1...
search query string Full-text: name, phone, email, tag jane

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/contacts" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/contacts", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Contacts page

{
  "contacts": [
    {
      "_id": "...",
      "phone": "+1...",
      "name": "Jane",
      "status": "subscribed",
      "tags": [
        "vip"
      ]
    }
  ],
  "total": 1240,
  "page": 1,
  "limit": 50
}
GET /api/mgmt/v1/contacts/stats Management host API key

Contact statistics

Aggregate counts: total, subscribed, and per-segment breakdown.

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/contacts/stats" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/contacts/stats", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Stats

{
  "total": 1240,
  "subscribed": 1100,
  "subscribedPercent": 88.7,
  "segments": [
    {
      "name": "VIPs",
      "contactCount": 230
    }
  ]
}
GET /api/mgmt/v1/contacts/:id Management host API key

Get a contact

Returns a single contact by ObjectId, enriched with a freshly-computed engagementScore and delivered-message count in behaviorProfile.

Parameters

NameInTypeDescription
id * path string Contact ObjectId 64f1...

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/contacts/{id}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/contacts/{id}", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Contact

{
  "_id": "...",
  "phone": "+12025551234",
  "name": "Jane Doe",
  "status": "subscribed",
  "tags": [
    "vip"
  ],
  "behaviorProfile": {
    "messagesSent": 12,
    "messagesDelivered": 11,
    "engagementScore": 72
  }
}
404

Not found

{
  "error": "Contact not found"
}
POST /api/mgmt/v1/contacts Management host API key

Create a contact

Creates a new contact. Phone number must be unique within the org.

Request body

{
  "phone": "+12025551234",
  "name": "Jane Doe",
  "email": "jane@example.com",
  "status": "subscribed",
  "tags": [
    "newsletter"
  ]
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/contacts" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+12025551234","name":"Jane Doe","email":"jane@example.com","status":"subscribed","tags":["newsletter"]}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/contacts", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "phone": "+12025551234",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "status": "subscribed",
    "tags": [
        "newsletter"
    ]
}),
});
const data = await res.json();
console.log(data);

Responses

201

Created

{
  "_id": "...",
  "phone": "+12025551234",
  "name": "Jane Doe",
  "status": "subscribed"
}
409

Phone already exists

{
  "error": "Contact with this phone already exists"
}
POST /api/mgmt/v1/contacts/import Management host API key

Bulk import contacts

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.

Request body

{
  "contacts": [
    {
      "phone": "+12025551234",
      "name": "Jane",
      "status": "subscribed"
    },
    {
      "phone": "+12025559999",
      "name": "Bob",
      "tags": [
        "vip"
      ]
    }
  ],
  "addToSegment": {
    "name": "Expo leads"
  }
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/contacts/import" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"contacts":[{"phone":"+12025551234","name":"Jane","status":"subscribed"},{"phone":"+12025559999","name":"Bob","tags":["vip"]}],"addToSegment":{"name":"Expo leads"}}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/contacts/import", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "contacts": [
        {
            "phone": "+12025551234",
            "name": "Jane",
            "status": "subscribed"
        },
        {
            "phone": "+12025559999",
            "name": "Bob",
            "tags": [
                "vip"
            ]
        }
    ],
    "addToSegment": {
        "name": "Expo leads"
    }
}),
});
const data = await res.json();
console.log(data);

Responses

200

Import result

{
  "imported": 1,
  "skipped": 1,
  "segment": {
    "_id": "...",
    "name": "Expo leads",
    "created": true,
    "added": 2,
    "contactCount": 2
  }
}
400

Invalid payload, or a live segment chosen

{
  "error": "contacts array is required"
}
404

Chosen segment not found

{
  "error": "The chosen segment no longer exists."
}
409

New segment name already used

{
  "error": "A segment named \"Expo leads\" already exists. Choose it from the list, or use a different name."
}
PUT /api/mgmt/v1/contacts/:id Management host API key

Update a contact

Full update of all editable contact fields.

Parameters

NameInTypeDescription
id * path string Contact ObjectId 64f1...

Request body

{
  "name": "Jane Smith",
  "tags": [
    "vip",
    "newsletter"
  ],
  "status": "subscribed"
}

cURL

curl -X PUT "https://portal.relay.cequens.com/api/mgmt/v1/contacts/{id}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Jane Smith","tags":["vip","newsletter"],"status":"subscribed"}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/contacts/{id}", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "name": "Jane Smith",
    "tags": [
        "vip",
        "newsletter"
    ],
    "status": "subscribed"
}),
});
const data = await res.json();
console.log(data);

Responses

200

Updated contact

{
  "_id": "...",
  "phone": "+12025551234",
  "name": "Jane Smith"
}
404

Not found

{
  "error": "Contact not found"
}
GET /api/v1/contacts/:phone Messaging host API key

Get contact profile (Gateway)

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

Parameters

NameInTypeDescription
phone * path string E.164 phone (URL-encoded) %2B12025551234

cURL

curl -X GET "https://ops.relay.cequens.com/api/v1/contacts/{phone}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/contacts/{phone}", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Contact profile

{
  "phone": "+12025551234",
  "name": "Jane Doe",
  "status": "subscribed",
  "tags": [
    "vip"
  ],
  "attributes": {
    "loyalty_tier": "gold"
  },
  "updatedAt": "2025-03-01T12:00:00Z"
}
404

Not found

{
  "error": "Contact not found"
}
POST /api/v1/contacts/:phone/tags Messaging host API key

Add tags to a contact (Gateway)

Appends tags without removing existing ones. Creates the contact if it does not exist. Served by the SMS gateway host, not the portal host.

Parameters

NameInTypeDescription
phone * path string E.164 phone (URL-encoded) %2B12025551234

Request body

{
  "tags": [
    "vip",
    "promo-june"
  ]
}

cURL

curl -X POST "https://ops.relay.cequens.com/api/v1/contacts/{phone}/tags" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"tags":["vip","promo-june"]}'

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/contacts/{phone}/tags", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "tags": [
        "vip",
        "promo-june"
    ]
}),
});
const data = await res.json();
console.log(data);

Responses

200

Updated tag list

{
  "phone": "+12025551234",
  "tags": [
    "newsletter",
    "vip",
    "promo-june"
  ]
}
400

Invalid payload

{
  "error": "tags must be a non-empty array of strings"
}
PUT /api/v1/contacts/:phone/tags Messaging host API key

Replace all tags (Gateway)

Overwrites the entire tag list. Send [] to clear all tags. Served by the SMS gateway host, not the portal host.

Parameters

NameInTypeDescription
phone * path string E.164 phone (URL-encoded) %2B12025551234

Request body

{
  "tags": [
    "newsletter"
  ]
}

cURL

curl -X PUT "https://ops.relay.cequens.com/api/v1/contacts/{phone}/tags" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"tags":["newsletter"]}'

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/contacts/{phone}/tags", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "tags": [
        "newsletter"
    ]
}),
});
const data = await res.json();
console.log(data);

Responses

200

New tag list

{
  "phone": "+12025551234",
  "tags": [
    "newsletter"
  ]
}
DELETE /api/v1/contacts/:phone/tags Messaging host API key

Remove tags from a contact (Gateway)

Removes the specified tags. Tags not present are silently ignored. Served by the SMS gateway host, not the portal host.

Parameters

NameInTypeDescription
phone * path string E.164 phone (URL-encoded) %2B12025551234

Request body

{
  "tags": [
    "promo-june"
  ]
}

cURL

curl -X DELETE "https://ops.relay.cequens.com/api/v1/contacts/{phone}/tags" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"tags":["promo-june"]}'

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/contacts/{phone}/tags", {
  method: "DELETE",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "tags": [
        "promo-june"
    ]
}),
});
const data = await res.json();
console.log(data);

Responses

200

Remaining tags

{
  "phone": "+12025551234",
  "tags": [
    "newsletter",
    "vip"
  ]
}
404

Contact not found

{
  "error": "Contact not found"
}
PATCH /api/v1/contacts/:phone/attributes Messaging host API key

Set CDP attributes (Gateway)

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.

Parameters

NameInTypeDescription
phone * path string E.164 phone (URL-encoded) %2B12025551234

Request body

{
  "attributes": {
    "loyalty_tier": "gold",
    "last_purchase": "2025-03-01",
    "preferred_language": "en"
  }
}

cURL

curl -X PATCH "https://ops.relay.cequens.com/api/v1/contacts/{phone}/attributes" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"attributes":{"loyalty_tier":"gold","last_purchase":"2025-03-01","preferred_language":"en"}}'

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/contacts/{phone}/attributes", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "attributes": {
        "loyalty_tier": "gold",
        "last_purchase": "2025-03-01",
        "preferred_language": "en"
    }
}),
});
const data = await res.json();
console.log(data);

Responses

200

Updated attributes

{
  "phone": "+12025551234",
  "attributes": {
    "loyalty_tier": "gold",
    "last_purchase": "2025-03-01",
    "preferred_language": "en"
  }
}
400

Invalid payload

{
  "error": "attributes must be a key-value object"
}
DELETE /api/v1/contacts/:phone/attributes/:key Messaging host API key

Remove a CDP attribute (Gateway)

Removes a single CDP attribute from the contact. Served by the SMS gateway host, not the portal host.

Parameters

NameInTypeDescription
phone * path string E.164 phone (URL-encoded) %2B12025551234
key * path string Attribute key to delete loyalty_tier

cURL

curl -X DELETE "https://ops.relay.cequens.com/api/v1/contacts/{phone}/attributes/{key}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/contacts/{phone}/attributes/{key}", {
  method: "DELETE",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Remaining attributes

{
  "phone": "+12025551234",
  "attributes": {
    "last_purchase": "2025-03-01"
  }
}
404

Contact not found

{
  "error": "Contact not found"
}

Segments

Create static or dynamic contact segments for campaign targeting.

GET /api/mgmt/v1/segments Management host API key

List segments

Returns all segments. Dynamic segments have their contactCount refreshed on each call.

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/segments" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/segments", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Segments

[
  {
    "_id": "...",
    "name": "VIPs",
    "type": "static",
    "contactCount": 230
  }
]
POST /api/mgmt/v1/segments Management host API key

Create a segment

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.

Request body

{
  "name": "Riyadh VIPs — Ramadan list",
  "membership": "fixed",
  "query": "city = Riyadh, vip"
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/segments" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Riyadh VIPs — Ramadan list","membership":"fixed","query":"city = Riyadh, vip"}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/segments", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "name": "Riyadh VIPs — Ramadan list",
    "membership": "fixed",
    "query": "city = Riyadh, vip"
}),
});
const data = await res.json();
console.log(data);

Responses

201

Created

{
  "_id": "...",
  "name": "Riyadh VIPs — Ramadan list",
  "type": "static",
  "contactCount": 412,
  "truncated": false
}
400

Invalid rule, or a live rule on top of another live segment

{
  "error": "A live segment can't be built on another live segment. Save it as a fixed segment instead."
}
PUT /api/mgmt/v1/segments/:id Management host API key

Update a segment

Update name, description, or filter rules.

Parameters

NameInTypeDescription
id * path string Segment ObjectId 64f1...

Request body

{
  "name": "Premium buyers",
  "description": "Updated description"
}

cURL

curl -X PUT "https://portal.relay.cequens.com/api/mgmt/v1/segments/{id}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Premium buyers","description":"Updated description"}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/segments/{id}", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "name": "Premium buyers",
    "description": "Updated description"
}),
});
const data = await res.json();
console.log(data);

Responses

200

Updated

{
  "_id": "...",
  "name": "Premium buyers"
}
404

Not found

{
  "error": "Segment not found"
}
POST /api/mgmt/v1/segments/:id/contacts Management host API key

Add contacts to segment

Adds an array of contact IDs (max 5,000) to a fixed (static) segment. Live segments refuse manual additions.

Parameters

NameInTypeDescription
id * path string Segment ObjectId 64f1...

Request body

{
  "contactIds": [
    "64f1...",
    "64f2..."
  ]
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/segments/{id}/contacts" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"contactIds":["64f1...","64f2..."]}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/segments/{id}/contacts", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "contactIds": [
        "64f1...",
        "64f2..."
    ]
}),
});
const data = await res.json();
console.log(data);

Responses

200

Added

{
  "added": 2,
  "contactCount": 232
}
GET /api/mgmt/v1/segments/:id/contacts Management host API key

List contacts in segment

Returns paginated contacts belonging to the segment.

Parameters

NameInTypeDescription
id * path string Segment ObjectId 64f1...
page query integer Page number 1
limit query integer Items per page (max 200) 50

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/segments/{id}/contacts" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/segments/{id}/contacts", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Contacts page

{
  "contacts": [],
  "total": 230,
  "page": 1,
  "limit": 50
}
POST /api/mgmt/v1/segments/:id/populate Management host API key

Auto-populate from filter

Applies a filter descriptor (from AI suggestions) to add matching contacts. Supported filter.type values: engagementScore, tag, cdpAttribute, subscribed.

Parameters

NameInTypeDescription
id * path string Segment ObjectId 64f1...

Request body

{
  "filter": {
    "type": "engagementScore",
    "min": 70
  }
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/segments/{id}/populate" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"filter":{"type":"engagementScore","min":70}}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/segments/{id}/populate", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "filter": {
        "type": "engagementScore",
        "min": 70
    }
}),
});
const data = await res.json();
console.log(data);

Responses

200

Contacts added

{
  "added": 145
}

Campaigns

Create, manage, and broadcast SMS campaigns to contact segments.

GET /api/mgmt/v1/campaigns Management host API key

List campaigns

Paginated campaign list with optional status filtering.

Parameters

NameInTypeDescription
status query string draft | queued | sending | completed | failed | paused sending
page query integer Page number 1
limit query integer Items per page (max 100) 20

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/campaigns" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/campaigns", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Campaigns page

{
  "campaigns": [
    {
      "_id": "...",
      "name": "Summer Sale",
      "status": "completed",
      "estimatedReach": 500
    }
  ],
  "total": 12,
  "page": 1
}
POST /api/mgmt/v1/campaigns Management host API key

Create a campaign

Creates a campaign in draft status. Call the launch endpoint when ready.

Request body

{
  "name": "Summer Sale",
  "senderId": "BRAND",
  "content": "Hi {name}, get 20% off today! Reply STOP to unsubscribe.",
  "targetSegments": [
    "seg-id"
  ],
  "schedule": {
    "type": "immediate"
  }
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/campaigns" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Summer Sale","senderId":"BRAND","content":"Hi {name}, get 20% off today! Reply STOP to unsubscribe.","targetSegments":["seg-id"],"schedule":{"type":"immediate"}}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/campaigns", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "name": "Summer Sale",
    "senderId": "BRAND",
    "content": "Hi {name}, get 20% off today! Reply STOP to unsubscribe.",
    "targetSegments": [
        "seg-id"
    ],
    "schedule": {
        "type": "immediate"
    }
}),
});
const data = await res.json();
console.log(data);

Responses

201

Draft created

{
  "_id": "...",
  "name": "Summer Sale",
  "status": "draft"
}
GET /api/mgmt/v1/campaigns/:id Management host API key

Get a campaign

Returns a single campaign with enriched delivery metrics.

Parameters

NameInTypeDescription
id * path string Campaign ObjectId 64f1...

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Campaign

{
  "_id": "...",
  "name": "Summer Sale",
  "status": "completed",
  "liveMetrics": {
    "sent": 450,
    "delivered": 430,
    "failed": 20
  }
}
404

Not found

{
  "error": "Campaign not found"
}
PUT /api/mgmt/v1/campaigns/:id Management host API key

Update a campaign

Update name, content, schedule, or targeting of a draft or paused campaign.

Parameters

NameInTypeDescription
id * path string Campaign ObjectId 64f1...

Request body

{
  "name": "Summer Sale (Final)",
  "content": "Updated message here."
}

cURL

curl -X PUT "https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Summer Sale (Final)","content":"Updated message here."}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "name": "Summer Sale (Final)",
    "content": "Updated message here."
}),
});
const data = await res.json();
console.log(data);

Responses

200

Updated

{
  "_id": "...",
  "name": "Summer Sale (Final)"
}
404

Not found

{
  "error": "Campaign not found"
}
POST /api/mgmt/v1/campaigns/:id/recipients/batch Management host API key

Upload CSV recipients

Uploads a batch of phone numbers as campaign recipients. Set replace: true to overwrite the existing list.

Parameters

NameInTypeDescription
id * path string Campaign ObjectId 64f1...

Request body

{
  "recipients": [
    {
      "phone": "+12025551234",
      "name": "Jane"
    },
    {
      "phone": "+12025559999"
    }
  ],
  "replace": false
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}/recipients/batch" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"recipients":[{"phone":"+12025551234","name":"Jane"},{"phone":"+12025559999"}],"replace":false}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}/recipients/batch", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "recipients": [
        {
            "phone": "+12025551234",
            "name": "Jane"
        },
        {
            "phone": "+12025559999"
        }
    ],
    "replace": false
}),
});
const data = await res.json();
console.log(data);

Responses

200

Accepted

{
  "accepted": 2,
  "total": 102
}
POST /api/mgmt/v1/campaigns/:id/launch Management host API key

Launch a campaign

Publishes messages to all matching contacts via NATS. Checks credits and enforces trial restrictions.

Parameters

NameInTypeDescription
id * path string Campaign ObjectId 64f1...

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}/launch" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}/launch", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Launched

{
  "message": "Campaign launched",
  "recipientCount": 450,
  "status": "sending"
}
402

Insufficient credits

{
  "error": "Insufficient credits"
}
403

Trial quota exhausted

{
  "error": "Trial quota exhausted. Please upgrade to a paid plan."
}
400

No contacts match

{
  "error": "No contacts match the target criteria"
}
POST /api/mgmt/v1/campaigns/:id/pause Management host API key

Pause a sending campaign

Pauses a campaign that is currently in sending status.

Parameters

NameInTypeDescription
id * path string Campaign ObjectId 64f1...

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}/pause" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}/pause", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Paused

{
  "_id": "...",
  "status": "paused"
}
404

Not found or not sending

{
  "error": "Campaign not found or not sending"
}
GET /api/mgmt/v1/campaigns/:id/metrics Management host API key

Campaign delivery metrics

Returns live sent / delivered / failed counts from the shared message log.

Parameters

NameInTypeDescription
id * path string Campaign ObjectId 64f1...

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}/metrics" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/campaigns/{id}/metrics", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Metrics

{
  "_id": "...",
  "name": "Summer Sale",
  "liveMetrics": {
    "sent": 450,
    "delivered": 430,
    "failed": 20
  }
}

SMS

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/senders Management host API key

Get available sender IDs

Returns all approved sender IDs for the organisation.

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/senders" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/senders", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Sender IDs

[
  "BRAND",
  "ALERTS",
  "PROMO"
]
POST /api/v1/sms/messages Messaging host API key

Send an SMS

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.

{
  "from": "BRAND",
  "to": "+12025551234",
  "text": "Hello from Relay!",
  "campaignId": "64f1..."
}

cURL

curl -X POST "https://ops.relay.cequens.com/api/v1/sms/messages" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"from":"BRAND","to":"+12025551234","text":"Hello from Relay!","campaignId":"64f1..."}'

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/sms/messages", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "from": "BRAND",
    "to": "+12025551234",
    "text": "Hello from Relay!",
    "campaignId": "64f1..."
}),
});
const data = await res.json();
console.log(data);

Responses

202

Message accepted

{
  "messageId": "uuid",
  "status": "queued",
  "clientId": "CPAAS-XXXX"
}
401

Missing or invalid API key

{
  "error": "Missing API key"
}
422

Rejected (blocklist / quota)

{
  "status": "rejected",
  "reason": "Destination on DND list"
}
400

Missing required fields

{
  "error": "from, to, and text are required"
}
POST /api/v1/sms/messages/bulk Messaging host API key

Bulk send SMS

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.

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

cURL

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

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/sms/messages/bulk", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "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..."
}),
});
const data = await res.json();
console.log(data);

Responses

202

Queued (full or partial)

{
  "queued": 2,
  "rejected": 1,
  "messageIds": [
    {
      "to": "+12025551001",
      "messageId": "uuid"
    },
    {
      "to": "+12025551002",
      "messageId": "uuid"
    }
  ],
  "rejections": [
    {
      "to": "+12025551003",
      "reason": "quota_exhausted"
    }
  ]
}
400

Validation error

{
  "error": "recipients must be a non-empty array of up to 1000 items"
}
401

Missing or invalid API key

{
  "error": "Missing API key (Authorization: Bearer <key> or X-API-Key header)"
}
403

Sender not approved / app paused / IP blocked

{
  "error": "Sender ID \"BRAND\" is not in the approved senders list for this account"
}
429

TPS cap reached

{
  "error": "TPS cap of 50 msg/s reached"
}
GET /api/mgmt/v1/messages/stats Management host API key

Message statistics

Aggregate counts by status and average latency for the requested window.

Parameters

NameInTypeDescription
from query string ISO 8601 start date 2025-03-01
to query string ISO 8601 end date 2025-03-31
status query string Queued | Sent | Delivered | Failed | Unknown | … Failed
appId query string Filter to a specific Project 64f1...

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/messages/stats" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/messages/stats", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Stats

{
  "total": 12400,
  "byStatus": {
    "Delivered": 11800,
    "Failed": 420,
    "Sent": 150,
    "Queued": 30
  },
  "avgLatencyMs": 312
}
GET /api/mgmt/v1/messages Management host API key

List message logs

Paginated log of outbound messages for the last 90 days.

Parameters

NameInTypeDescription
page query integer Page number 1
limit query integer Items per page (max 200) 50
status query string Queued (in Relay) | Sent (accepted by the carrier, awaiting receipt) | Delivered | Failed | Unknown | Rejected | Blocked Failed
from query string ISO 8601 start date 2025-03-01
to query string ISO 8601 end date 2025-03-31
search query string Search sender, destination, msgId, text BRAND
appId query string Filter to a Project ObjectId 64f1...

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/messages" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/messages", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Logs

{
  "logs": [
    {
      "messageId": "uuid",
      "sender": "BRAND",
      "destination": "+1...",
      "status": "Delivered",
      "latencyMs": 320
    }
  ],
  "total": 12400,
  "page": 1,
  "pages": 248
}
GET /api/mgmt/v1/messages/export Management host API key

Export messages as CSV

Streams a CSV file (up to 10,000 rows) matching the filters. Sets Content-Disposition: attachment.

Parameters

NameInTypeDescription
status query string Filter by status Failed
from query string ISO 8601 start date 2025-03-01
to query string ISO 8601 end date 2025-03-31
appId query string Filter by Project 64f1...

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/messages/export" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/messages/export", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

CSV download

{
  "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.

POST /api/v1/whatsapp/messages Messaging host API key

Send a template

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.

{
  "from": "+31644102243",
  "to": "+12025551234",
  "type": "template",
  "template": {
    "name": "order_shipping",
    "language": "en",
    "variables": {
      "1": "Omar Rodrigues",
      "2": "omar@example.com"
    }
  }
}

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":"template","template":{"name":"order_shipping","language":"en","variables":{"1":"Omar Rodrigues","2":"omar@example.com"}}}'

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": "template",
    "template": {
        "name": "order_shipping",
        "language": "en",
        "variables": {
            "1": "Omar Rodrigues",
            "2": "omar@example.com"
        }
    }
}),
});
const data = await res.json();
console.log(data);

Responses

202

Queued for delivery

{
  "messageId": "wa_lxky9z_a1b2c3d4",
  "status": "queued"
}
200

Duplicate idempotency key

{
  "messageId": "wa_lxky9z_a1b2c3d4",
  "status": "sent",
  "deduplicated": true
}
400

Missing or invalid fields

{
  "error": "`from`, `to`, and `type` are required"
}
401

Missing or invalid API key

{
  "error": "Missing API key (Authorization: Bearer <key> or X-API-Key header)"
}
404

Sender not found for this account

{
  "error": "Sender +31644102243 not found for this account"
}
409

Sender not Registered on Cloud API

{
  "error": "Sender is not Registered (state: Pending)"
}
422

Template / suppression failure

{
  "error": "Template 'order_shipping' (en) is not APPROVED (status: PENDING)"
}
429

TPS cap reached (per channel)

{
  "error": "TPS cap of 50 msg/s reached",
  "code": "throughput_exceeded"
}
POST /api/v1/whatsapp/messages Messaging host API key

Send a text reply

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"
}
POST /api/v1/whatsapp/messages Messaging host API key

Send media

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)"
}
GET /api/v1/whatsapp/messages/:messageId Messaging host API key

Get message status

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

NameInTypeDescription
messageId * path string The messageId returned by POST /api/v1/whatsapp/messages wa_lxky9z_a1b2c3d4

cURL

curl -X GET "https://ops.relay.cequens.com/api/v1/whatsapp/messages/{messageId}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/whatsapp/messages/{messageId}", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Current status

{
  "messageId": "wa_lxky9z_a1b2c3d4",
  "wamid": "wamid.HBgL…",
  "status": "delivered",
  "type": "template",
  "contactPhone": "+12025551234",
  "phoneNumberId": "1136187512911204",
  "sentAt": "2026-05-21T10:00:00Z",
  "deliveredAt": "2026-05-21T10:00:02Z"
}
404

Not found

{
  "error": "Message not found"
}

Email

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/messages Messaging host API 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.

{
  "from": {
    "identityId": "665f1c2a9b4e2f0012a34bcd"
  },
  "to": "omar@example.com",
  "templateId": "665f1c2a9b4e2f0012a34bce",
  "locale": "en",
  "variables": {
    "first_name": "Omar",
    "order_id": "SA-10233"
  }
}

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":{"identityId":"665f1c2a9b4e2f0012a34bcd"},"to":"omar@example.com","templateId":"665f1c2a9b4e2f0012a34bce","locale":"en","variables":{"first_name":"Omar","order_id":"SA-10233"}}'

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": {
        "identityId": "665f1c2a9b4e2f0012a34bcd"
    },
    "to": "omar@example.com",
    "templateId": "665f1c2a9b4e2f0012a34bce",
    "locale": "en",
    "variables": {
        "first_name": "Omar",
        "order_id": "SA-10233"
    }
}),
});
const data = await res.json();
console.log(data);

Responses

202

Queued for delivery

{
  "messageId": "em_lxky9z_a1b2c3d4",
  "status": "queued"
}
200

Duplicate idempotency key

{
  "messageId": "em_lxky9z_a1b2c3d4",
  "status": "sent",
  "deduplicated": true
}
400

Missing or invalid fields

{
  "error": "`to` must be a valid email address"
}
401

Missing or invalid API key

{
  "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/messages Messaging host API 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);

Responses

202

Queued for delivery

{
  "messageId": "em_lxky9z_a1b2c3d4",
  "status": "queued"
}
403

Raw HTML needs your own domain

{
  "error": "Raw HTML sends require a verified own domain; use a template with a Relay address",
  "code": "raw_requires_domain"
}
413

Body too large

{
  "error": "Email body exceeds 9 MB"
}
GET /api/v1/email/messages/:messageId Messaging host API key

Get message status

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

NameInTypeDescription
messageId * path string The messageId returned by POST /api/v1/email/messages em_lxky9z_a1b2c3d4

cURL

curl -X GET "https://ops.relay.cequens.com/api/v1/email/messages/{messageId}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/email/messages/{messageId}", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Current status

{
  "messageId": "em_lxky9z_a1b2c3d4",
  "sesMessageId": "0100018f…",
  "status": "delivered",
  "to": "omar@example.com",
  "from": "hello@mail.yourcompany.com",
  "subject": "Your order SA-10233 has shipped",
  "sentAt": "2026-05-21T10:00:00Z",
  "deliveredAt": "2026-05-21T10:00:03Z",
  "firstOpenedAt": "2026-05-21T10:14:22Z",
  "opens": 2,
  "clicks": 1
}
404

Not found

{
  "error": "Message not found"
}
GET /api/v1/email/identities Messaging host API key

List sender identities

Your verified sending identities and their default From addresses — use an _id from here as from.identityId when sending.

cURL

curl -X GET "https://ops.relay.cequens.com/api/v1/email/identities" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/email/identities", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Identities

{
  "items": [
    {
      "_id": "665f1c2a9b4e2f0012a34bcd",
      "kind": "domain",
      "displayName": "Your Brand",
      "domain": "mail.yourcompany.com",
      "defaultFrom": "hello@mail.yourcompany.com",
      "verification": {
        "status": "verified"
      },
      "isDefault": true
    }
  ]
}

Voice calls

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/calls Messaging host API 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.

{
  "to": "+966500000000",
  "message": {
    "text": "Your verification code is 4, 8, 1, 5, 9, 2.",
    "language": "en-US",
    "repeat": 2
  },
  "transactional": true
}

cURL

curl -X POST "https://ops.relay.cequens.com/api/v1/voice/calls" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"to":"+966500000000","message":{"text":"Your verification code is 4, 8, 1, 5, 9, 2.","language":"en-US","repeat":2},"transactional":true}'

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/voice/calls", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "to": "+966500000000",
    "message": {
        "text": "Your verification code is 4, 8, 1, 5, 9, 2.",
        "language": "en-US",
        "repeat": 2
    },
    "transactional": true
}),
});
const data = await res.json();
console.log(data);

Responses

202

Queued

{
  "callId": "66f1c0ffee0000000000beef",
  "status": "queued",
  "from": "+966110000000"
}
200

Repeat of an earlier request (same Idempotency-Key)

{
  "callId": "66f1c0ffee0000000000beef",
  "status": "queued",
  "from": "+966110000000",
  "deduplicated": true
}
400

Invalid request

{
  "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/:id Messaging host API 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

NameInTypeDescription
id * path string The callId returned by POST /api/v1/voice/calls 66f1c0ffee0000000000beef

cURL

curl -X GET "https://ops.relay.cequens.com/api/v1/voice/calls/{id}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://ops.relay.cequens.com/api/v1/voice/calls/{id}", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Call

{
  "_id": "66f1c0ffee0000000000beef",
  "direction": "outbound",
  "outboundKind": "message",
  "from": "+966110000000",
  "to": "+966500000000",
  "status": "completed",
  "endReason": "flow_end",
  "durationSec": 14,
  "billableSec": 60,
  "chargeMinor": 30,
  "currency": "SAR"
}
404

Not found

{
  "error": "Call not found"
}
POST /api/mgmt/v1/voice/calls Management host API key

Place a voice call (portal)

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.

Request body

{
  "agentId": "66f1c0ffee0000000000abcd",
  "to": "+966500000000",
  "variables": {
    "firstName": "Sara",
    "appointment": "Tuesday 4 pm"
  }
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/voice/calls" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"agentId":"66f1c0ffee0000000000abcd","to":"+966500000000","variables":{"firstName":"Sara","appointment":"Tuesday 4 pm"}}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/voice/calls", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "agentId": "66f1c0ffee0000000000abcd",
    "to": "+966500000000",
    "variables": {
        "firstName": "Sara",
        "appointment": "Tuesday 4 pm"
    }
}),
});
const data = await res.json();
console.log(data);

Responses

202

Queued

{
  "callId": "66f1c0ffee0000000000beef",
  "status": "queued"
}
402

Balance too low for a call

{
  "error": "Your balance does not cover a call — top up your wallet",
  "code": "insufficient_funds"
}
403

Destination not enabled

{
  "error": "Calls to this destination are not enabled for your account",
  "code": "destination_not_allowed"
}
409

Quiet hours, opted-out contact, or inactive agent

{
  "error": "Calls are paused for this recipient’s quiet hours",
  "code": "quiet_hours",
  "retryAt": "2026-09-25T06:00:00.000Z"
}
429

Too many calls started per second

{
  "error": "Too many calls started in the last second — try again shortly",
  "code": "rate_limited"
}
GET /api/mgmt/v1/voice/calls Management host API key

List voice calls

Paginated call log, newest first, for inbound and outbound calls handled by your Voice AI agents.

Parameters

NameInTypeDescription
page query integer Page number 1
limit query integer Items per page (max 200) 50
direction query string inbound | outbound outbound
status query string queued | ringing | answered | completed | failed | no_answer | busy | rejected completed
agentId query string Only calls handled by this agent 66f1...
from query string ISO 8601 start date 2026-09-01
to query string ISO 8601 end date 2026-09-30
search query string Digits of the caller or called number 9665

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/voice/calls" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/voice/calls", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Calls

{
  "calls": [
    {
      "_id": "66f1...",
      "direction": "outbound",
      "from": "+966110000000",
      "to": "+966500000000",
      "status": "completed",
      "durationSec": 75,
      "billableSec": 120,
      "chargeMinor": 120,
      "currency": "SAR"
    }
  ],
  "total": 42,
  "page": 1,
  "pages": 1
}
GET /api/mgmt/v1/voice/calls/:id Management host API key

Get a voice call

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.

Parameters

NameInTypeDescription
id * path string Call id 66f1...

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/voice/calls/{id}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/voice/calls/{id}", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Call

{
  "_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/:slug Management 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

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

{
  "phone": "+966500000000",
  "order_id": "ORD-1001",
  "amount": 249.5,
  "customer": {
    "tier": "gold"
  }
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/webhooks/inbound/{slug}" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+966500000000","order_id":"ORD-1001","amount":249.5,"customer":{"tier":"gold"}}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/webhooks/inbound/{slug}", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
},
  body: JSON.stringify({
    "phone": "+966500000000",
    "order_id": "ORD-1001",
    "amount": 249.5,
    "customer": {
        "tier": "gold"
    }
}),
});
const data = await res.json();
console.log(data);

Responses

202

Run started

{
  "enrollmentId": "66f1c0ffee0000000000e001"
}
202

A waiting run was resumed

{
  "resumed": true,
  "enrollmentId": "66f1c0ffee0000000000e001"
}
400

Required fields missing

{
  "error": "missing_fields",
  "message": "Missing required fields: order_id, amount",
  "missing": [
    "order_id",
    "amount"
  ]
}
400

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.

GET /api/mgmt/v1/apps Management host API key

List projects

Returns all projects for the organisation.

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/apps" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/apps", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Projects

[
  {
    "_id": "...",
    "name": "My Project",
    "status": "active",
    "monthlyLimits": {
      "sms": 500000,
      "whatsapp": 50000,
      "email": 250000,
      "voice": 5000
    }
  }
]
POST /api/mgmt/v1/apps Management host API key

Create a project

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.

Request body

{
  "name": "My Marketing Project",
  "monthlyLimits": {
    "sms": 500000,
    "whatsapp": 50000,
    "email": 250000,
    "voice": 5000
  },
  "senders": [
    "BRAND"
  ]
}

cURL

curl -X POST "https://portal.relay.cequens.com/api/mgmt/v1/apps" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"My Marketing Project","monthlyLimits":{"sms":500000,"whatsapp":50000,"email":250000,"voice":5000},"senders":["BRAND"]}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/apps", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "name": "My Marketing Project",
    "monthlyLimits": {
        "sms": 500000,
        "whatsapp": 50000,
        "email": 250000,
        "voice": 5000
    },
    "senders": [
        "BRAND"
    ]
}),
});
const data = await res.json();
console.log(data);

Responses

201

Created

{
  "_id": "...",
  "name": "My Marketing Project",
  "status": "active"
}
GET /api/mgmt/v1/apps/:id Management host API key

Get a project

Returns a single project by ID.

Parameters

NameInTypeDescription
id * path string Project ObjectId 64f1...

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Project

{
  "_id": "...",
  "name": "My Project",
  "status": "active",
  "monthlyLimits": {
    "sms": 500000,
    "whatsapp": 50000,
    "email": 250000,
    "voice": 5000
  }
}
PUT /api/mgmt/v1/apps/:id Management host API key

Update a project

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.

Parameters

NameInTypeDescription
id * path string Project ObjectId 64f1...

Request body

{
  "monthlyLimits": {
    "sms": 750000,
    "whatsapp": 50000,
    "email": 0,
    "voice": 0
  }
}

cURL

curl -X PUT "https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"monthlyLimits":{"sms":750000,"whatsapp":50000,"email":0,"voice":0}}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}", {
  method: "PUT",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "monthlyLimits": {
        "sms": 750000,
        "whatsapp": 50000,
        "email": 0,
        "voice": 0
    }
}),
});
const data = await res.json();
console.log(data);

Responses

200

Updated

{
  "_id": "...",
  "monthlyLimits": {
    "sms": 750000,
    "whatsapp": 50000,
    "email": 0,
    "voice": 0
  }
}
PATCH /api/mgmt/v1/apps/:id/status Management host API key

Toggle project status

Activates or pauses the project. While paused, every send that routes through the project is rejected — SMS, WhatsApp and email alike.

Parameters

NameInTypeDescription
id * path string Project ObjectId 64f1...

Request body

{
  "status": "paused"
}

cURL

curl -X PATCH "https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}/status" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"status":"paused"}'

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}/status", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
  body: JSON.stringify({
    "status": "paused"
}),
});
const data = await res.json();
console.log(data);

Responses

200

Updated

{
  "_id": "...",
  "status": "paused"
}
GET /api/mgmt/v1/apps/:id/stats Management host API key

Project SMS delivery stats

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.

Parameters

NameInTypeDescription
id * path string Project ObjectId 64f1...
from query string ISO 8601 start 2025-03-01
to query string ISO 8601 end 2025-03-31

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}/stats" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}/stats", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Stats

[
  {
    "date": "2025-03-01",
    "sent": 420,
    "delivered": 400
  }
]
GET /api/mgmt/v1/apps/:id/usage Management host API key

Project monthly usage vs limits

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.

Parameters

NameInTypeDescription
id * path string Project ObjectId 64f1...

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}/usage" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/apps/{id}/usage", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Usage

{
  "month": "2026-09",
  "metered": true,
  "channels": [
    {
      "channel": "sms",
      "unit": "parts",
      "limit": 500000,
      "used": 128430
    },
    {
      "channel": "whatsapp",
      "unit": "messages",
      "limit": 50000,
      "used": 9120
    },
    {
      "channel": "email",
      "unit": "messages",
      "limit": 0,
      "used": 41207
    }
  ]
}

Billing

Subscription management, credit purchases, and invoice history.

GET /api/mgmt/v1/billing/subscription Management host API key

Get subscription

Returns current plan, credit balance, and billing cycle.

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/billing/subscription" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/billing/subscription", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Subscription

{
  "plan": "pro",
  "status": "active",
  "credits": {
    "total": 5000,
    "used": 1200,
    "resetDate": "2025-04-01"
  },
  "monthlyPrice": 49
}
GET /api/mgmt/v1/billing/package Management host API key

Plan, wallet & usage details

Returns the assigned plan, included quotas, prepaid wallet balance, and current-month usage counts.

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/billing/package" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/billing/package", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Plan & wallet info

{
  "currency": "SAR",
  "monthlyUsed": 8400,
  "partsUsed": 9100,
  "plan": {
    "name": "Growth",
    "isFree": false,
    "includedContacts": 30000,
    "includedSeats": 8,
    "includedAiConversations": 1500,
    "includedWaNumbers": 3
  },
  "wallet": {
    "balance": 420.5,
    "formatted": "ر.س420.50"
  },
  "smsPriceMinor": 14,
  "monthStart": "2026-06-01T00:00:00Z"
}
GET /api/mgmt/v1/billing/plans Management host API key

Available plans

Returns all self-service plans that can be subscribed to.

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/billing/plans" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/billing/plans", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Plans

[
  {
    "id": "...",
    "key": "starter",
    "name": "Starter",
    "isFree": false,
    "price": 19,
    "priceFormatted": "$19.00",
    "currency": "USD",
    "includedContacts": 5000,
    "includedSeats": 3,
    "includedAiConversations": 500,
    "includedWaNumbers": 1,
    "smsFreeCap": 0,
    "featureFlags": {}
  }
]
GET /api/mgmt/v1/billing/invoices Management host API key

List invoices

Invoice history sorted by most recent.

Parameters

NameInTypeDescription
limit query integer Max results (default 20, max 100) 20

cURL

curl -X GET "https://portal.relay.cequens.com/api/mgmt/v1/billing/invoices" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json"

JavaScript

const res = await fetch("https://portal.relay.cequens.com/api/mgmt/v1/billing/invoices", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer <api_key>"
},
});
const data = await res.json();
console.log(data);

Responses

200

Invoices

[
  {
    "invoiceNumber": "INV-123",
    "amount": 49,
    "currency": "USD",
    "status": "paid",
    "paidAt": "2025-03-01T00:00:00Z"
  }
]