Cast API

Send SMS, WhatsApp, and voice messages. Trigger flows. Manage contacts, properties, and events. Create short links. Receive delivery webhooks. All through one REST API. Built for MENA + Africa.

Overview

Base URL. All API endpoints are under:

https://api.cast.wayvzgroup.com

The API speaks JSON: send Content-Type: application/json on POST requests; all responses are JSON. The X-Api-Version: v1 header is included on every response.

Quick test — verify your key

curl "https://api.cast.wayvzgroup.com/v1/ping" \
  -H "Authorization: Bearer sk_live_..."

Endpoint index

MethodPathWhat it does
GET/v1/pingHealth check + auth test
GET/v1/balanceCurrent wallet balance
GET/v1/accountAccount profile + rate limits
POST/v1/messagesSend SMS (single or bulk)
GET/v1/messagesList recent messages
GET/v1/messages/{id}Get a single message by ID
POST/v1/whatsapp/messagesSend a WhatsApp template message
POST/v1/voice/callPlace a voice call (TTS + DTMF)
GET/v1/voice/call/{callSid}Voice call status + duration + DTMF
GET/v1/flowsList your flows + 24h enrollment counts
POST/v1/flows/{id}/triggerStart a flow for a contact (with merge-tag variables)
GET/v1/flows/enrollments/{id}Enrollment detail with step-by-step timeline
POST/v1/contactsCreate or update a contact (idempotent on phone)
GET/v1/contactsList contacts (paginated, filter by group + search)
GET/v1/contacts/{phone}Get contact with properties + groups + tags
GET/v1/contacts/{phone}/propertiesList a contact’s custom properties
POST/v1/contacts/{phone}/propertiesUpsert one or more properties
GET/v1/contacts/{phone}/eventsList recent events for a contact
POST/v1/contacts/{phone}/eventsEmit a custom event
POST/v1/linksCreate a slnk.ai short link
POST/v1/campaignsSend a multi-recipient campaign (sms / whatsapp / voice)

Authentication

Cast uses Bearer tokens. Create a key at /developers, then send it on every request:

Authorization: Bearer sk_live_...

Keys start with sk_live_ followed by 32 random characters. They are shown once at creation time — store them safely. Lost keys can be revoked and replaced from the same page.

Keep keys server-side. Anyone with your key can spend your wallet. Never bundle API keys into a mobile or browser app.

Errors

Cast returns conventional HTTP status codes. Error bodies are always JSON:

{
  "error": {
    "code": "missing_field",
    "message": "Required field(s) missing: to"
  }
}

See the full error codes reference below.

SMS

POST /v1/messages
Send an SMS to one or many recipients. Pass a single number or an array in to for bulk sends (max 1,000 per request).

Request body

FieldTypeNotes
tostring | string[]requiredE.164 number(s). Plus sign optional. Array for bulk (max 1,000).
fromstringrequiredSender ID — up to 11 chars. Alphanumeric senders need per-country registration.
textstringrequiredMessage body.
languageintoptional0 = GSM-7 (default, 160 chars/segment), 1 = Unicode (70 chars/segment).
schedulestringoptionalISO 8601 datetime to defer sending.
curl -X POST "https://api.cast.wayvzgroup.com/v1/messages" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "9613xxxxxxx",
    "from": "Cast",
    "text": "Your code is 123456"
  }'
const res = await fetch(
  'https://api.cast.wayvzgroup.com/v1/messages',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_live_...',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      to: '9613xxxxxxx',
      from: 'Cast',
      text: 'Your code is 123456'
    })
  }
);
const data = await res.json();
console.log(data.id); // "msg_12345"
import requests

resp = requests.post(
    'https://api.cast.wayvzgroup.com/v1/messages',
    headers={
        'Authorization': 'Bearer sk_live_...',
        'Content-Type': 'application/json'
    },
    json={
        'to': '9613xxxxxxx',
        'from': 'Cast',
        'text': 'Your code is 123456'
    }
)
print(resp.json()['id'])  # "msg_12345"
$ch = curl_init('https://api.cast.wayvzgroup.com/v1/messages');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer sk_live_...',
        'Content-Type: application/json'
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'to'   => '9613xxxxxxx',
        'from' => 'Cast',
        'text' => 'Your code is 123456'
    ])
]);
$data = json_decode(curl_exec($ch), true);
echo $data['id']; // "msg_12345"
Response 201
{
  "id": "msg_12345",
  "object": "message",
  "status": "queued",
  "to": ["9613xxxxxxx"],
  "from": "Cast",
  "language": 0,
  "parts": 1,
  "cost": 0.04,
  "currency": "USD",
  "created_at": "2026-05-01T11:00:00Z"
}
GET /v1/messages
List recent messages, newest first.

Query parameters

limitint1–200, default 50.
offsetintOffset for pagination.
tostringFilter by recipient phone number.
statusstringqueued, sent, delivered, read, failed.
from_date / to_datestringISO 8601 range.
cURL example
curl "https://api.cast.wayvzgroup.com/v1/messages?limit=20&status=delivered" \
  -H "Authorization: Bearer sk_live_..."
GET /v1/messages/{id}
Retrieve a single message by ID. The id field from a send response (e.g. msg_12345) works with or without the msg_ prefix.
cURL example
curl "https://api.cast.wayvzgroup.com/v1/messages/msg_12345" \
  -H "Authorization: Bearer sk_live_..."

WhatsApp

WhatsApp messages must use a Meta-approved template. Submit and manage templates from the template editor. See Templates for approval tips.

POST /v1/whatsapp/messages
Send a WhatsApp template message to a single recipient.

Request body

FieldTypeNotes
tostringrequiredE.164 phone number, with or without leading +.
template_namestringrequiredExactly as shown in your approved templates list.
template_languagestringrequirede.g. en_US, ar.
template_categorystringrequiredUTILITY, MARKETING, or AUTHENTICATION. Affects per-message pricing.
template_paramsstring[]optionalBody variable values in order. Required when the template contains {{1}}, {{2}}, etc.
curl -X POST "https://api.cast.wayvzgroup.com/v1/whatsapp/messages" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "9613xxxxxxx",
    "template_name": "order_shipped",
    "template_language": "en_US",
    "template_category": "UTILITY",
    "template_params": ["Hadi", "AWB-12345"]
  }'
const res = await fetch(
  'https://api.cast.wayvzgroup.com/v1/whatsapp/messages',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_live_...',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      to: '9613xxxxxxx',
      template_name: 'order_shipped',
      template_language: 'en_US',
      template_category: 'UTILITY',
      template_params: ['Hadi', 'AWB-12345']
    })
  }
);
const data = await res.json();
console.log(data.meta_message_id);
import requests

resp = requests.post(
    'https://api.cast.wayvzgroup.com/v1/whatsapp/messages',
    headers={
        'Authorization': 'Bearer sk_live_...',
        'Content-Type': 'application/json'
    },
    json={
        'to': '9613xxxxxxx',
        'template_name': 'order_shipped',
        'template_language': 'en_US',
        'template_category': 'UTILITY',
        'template_params': ['Hadi', 'AWB-12345']
    }
)
print(resp.json()['meta_message_id'])
$ch = curl_init('https://api.cast.wayvzgroup.com/v1/whatsapp/messages');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer sk_live_...',
        'Content-Type: application/json'
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'to'                => '9613xxxxxxx',
        'template_name'     => 'order_shipped',
        'template_language' => 'en_US',
        'template_category' => 'UTILITY',
        'template_params'   => ['Hadi', 'AWB-12345']
    ])
]);
$data = json_decode(curl_exec($ch), true);
echo $data['meta_message_id'];
Response 201
{
  "id": "msg_12346",
  "object": "whatsapp_message",
  "status": "queued",
  "to": "9613xxxxxxx",
  "template_name": "order_shipped",
  "template_language": "en_US",
  "template_category": "UTILITY",
  "cost": 0.014,
  "currency": "USD",
  "meta_message_id": "wamid.HBgL...",
  "created_at": "2026-05-01T11:00:00Z"
}

Templates

WhatsApp requires Meta to approve message templates before they can be sent. Submit them from the template editor; approval typically takes 1–10 minutes.

Categories

  • UTILITY — transactional updates tied to a customer action (order shipped, OTP, appointment reminders). Lowest rate.
  • MARKETING — promotions, offers, re-engagement. Higher rate; opt-out compliance required.
  • AUTHENTICATION — one-time passwords. Lowest rate, strict format requirements.

Common rejection reasons

  • Template body looks promotional but was submitted as UTILITY.
  • Variables placed at the very start or end of the body with no surrounding text.
  • Missing or vague example values for body variables.
  • Header image too large, wrong aspect ratio, or text-heavy image.
  • Footer containing URLs, phone numbers, or currency symbols (restricted in some markets).
Tip. Button templates (URL, phone, quick reply, copy code) are supported in the template editor. Variables in button URLs use the same {{1}} syntax as body variables.

Contacts — Properties

Contact properties are arbitrary key-value pairs you store per phone number. They power segment conditions (e.g. plan = "premium", lifetimeSpend > 500) and flow branch logic. Values are always stored as strings; Cast auto-casts to numeric/date types for segment comparisons.

GET /v1/contacts/{phone}/properties
List all stored properties for a contact.
cURL example
curl "https://api.cast.wayvzgroup.com/v1/contacts/9613xxxxxxx/properties" \
  -H "Authorization: Bearer sk_live_..."
Response 200
{
  "object": "contact_properties",
  "phone": "9613xxxxxxx",
  "properties": {
    "plan": "premium",
    "lifetimeSpend": "1250.00",
    "lastSeen": "2026-04-15"
  }
}
POST /v1/contacts/{phone}/properties
Upsert one or more properties for a contact (PATCH semantics — only the keys you send are changed). The body is a flat JSON object of "key": "value" pairs. Max 50 properties per request. Set a value to null to delete a property.

Property key rules

  • Must start with a letter.
  • May contain letters, digits, dots (.), underscores (_), or hyphens (-).
  • Max 100 characters.
curl -X POST "https://api.cast.wayvzgroup.com/v1/contacts/9613xxxxxxx/properties" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "premium",
    "lifetimeSpend": "1250.00",
    "lastSeen": "2026-04-15"
  }'
await fetch(
  'https://api.cast.wayvzgroup.com/v1/contacts/9613xxxxxxx/properties',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_live_...',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      plan: 'premium',
      lifetimeSpend: '1250.00',
      lastSeen: '2026-04-15'
    })
  }
);
requests.post(
    'https://api.cast.wayvzgroup.com/v1/contacts/9613xxxxxxx/properties',
    headers={'Authorization': 'Bearer sk_live_...'},
    json={'plan': 'premium', 'lifetimeSpend': '1250.00', 'lastSeen': '2026-04-15'}
)
Response 200
{
  "object": "contact_properties",
  "phone": "9613xxxxxxx",
  "upserted": 3
}

Contacts — Events

Contact events are timestamped actions (e.g. purchase.completed, form.submitted, subscription.renewed). They power event-triggered automation flows and segment conditions. Event type must follow domain.action format — lowercase letters, digits, and underscores only.

GET /v1/contacts/{phone}/events
List recent events for a contact, newest first.
cURL example
curl "https://api.cast.wayvzgroup.com/v1/contacts/9613xxxxxxx/events" \
  -H "Authorization: Bearer sk_live_..."
POST /v1/contacts/{phone}/events
Emit a custom event for a contact. This can trigger event-triggered automation flows configured in your Cast account.

Request body

FieldTypeNotes
typestringrequiredEvent type in domain.action format. Max 50 chars. Example: purchase.completed.
payloadobjectoptionalArbitrary JSON object with event-specific data. Max 8 KB. Stored with the event and available in flow conditions.
occurred_atstringoptionalISO 8601 datetime. Defaults to now. Use for backfilling historical events.
curl -X POST "https://api.cast.wayvzgroup.com/v1/contacts/9613xxxxxxx/events" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "purchase.completed",
    "payload": {
      "orderId": "ORD-4829",
      "amount": 249.99,
      "currency": "USD"
    }
  }'
await fetch(
  'https://api.cast.wayvzgroup.com/v1/contacts/9613xxxxxxx/events',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer sk_live_...',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      type: 'purchase.completed',
      payload: { orderId: 'ORD-4829', amount: 249.99, currency: 'USD' }
    })
  }
);
requests.post(
    'https://api.cast.wayvzgroup.com/v1/contacts/9613xxxxxxx/events',
    headers={'Authorization': 'Bearer sk_live_...'},
    json={
        'type': 'purchase.completed',
        'payload': {'orderId': 'ORD-4829', 'amount': 249.99, 'currency': 'USD'}
    }
)
Response 201
{
  "object": "contact_event",
  "id": 9001,
  "phone": "9613xxxxxxx",
  "type": "purchase.completed",
  "occurred_at": "2026-05-01T11:00:00Z"
}
Automation tip. An event-triggered flow set to fire on purchase.completed will enroll this contact within the next processor cycle (roughly 60 seconds) and execute the configured steps — send a WhatsApp thank-you, wait 24 hours, then send an upsell message.

Voice

POST /v1/voice/call

Place a one-off voice call with TTS audio and optional DTMF capture. The call is queued immediately, then placed via your configured Voice (SIP) connector. Returns a callSid you can poll for status. Each call is billed against your wallet at the per-minute rate configured for the destination operator (30-second billable floor).

Request body

{
  "to": "+9613197119",
  "ttsText": "Hi {firstname}, your driver is two minutes away.",
  "language": "en",
  "voiceId": "en-US-Wavenet-D",
  "speed": 1.0,
  "messageType": "tts",
  "dtmf": {
    "options": { "1": "Confirmed", "2": "Cancel" },
    "timeoutSec": 15
  },
  "callbackUrl": "https://your-app.example.com/cast/voice-callback",
  "metadata": { "orderId": "4821", "driverId": "D-441" }
}

cURL

curl -X POST "https://api.cast.wayvzgroup.com/v1/voice/call" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+9613197119",
    "ttsText": "Your pickup is at Hamra Street",
    "language": "en",
    "dtmf": { "options": { "1": "Confirmed", "2": "Cancel" }, "timeoutSec": 15 }
  }'

Response 201 Created

{
  "id": "vc_abc12345",
  "callSid": "abc12345def67890",
  "status": "queued",
  "to": "+9613197119",
  "estimatedCost": 0.025,
  "createdAt": "2026-05-16T10:30:00Z"
}
GET /v1/voice/call/{callSid}

Poll a voice call for status, duration, and DTMF response. Returns the actual call cost once the call reaches a terminal state.

cURL

curl "https://api.cast.wayvzgroup.com/v1/voice/call/abc12345def67890" \
  -H "Authorization: Bearer sk_live_..."

Response

{
  "callSid": "abc12345def67890",
  "status": "completed",
  "to": "+9613197119",
  "duration": 42,
  "dtmfResponse": "1",
  "dtmfLabel": "Confirmed",
  "startedAt": "2026-05-16T10:30:00Z",
  "answeredAt": "2026-05-16T10:30:05Z",
  "completedAt": "2026-05-16T10:30:47Z",
  "cost": 0.035
}

Flows

GET /v1/flows

List the flows on your account. Optionally filter by status.

cURL

curl "https://api.cast.wayvzgroup.com/v1/flows?status=active" \
  -H "Authorization: Bearer sk_live_..."

Response

{
  "flows": [
    {
      "id": 42,
      "name": "Driver Notification",
      "status": "active",
      "nodeCount": 6,
      "enrollmentsLast24h": 487,
      "createdAt": "2026-05-01T00:00:00Z"
    }
  ],
  "total": 1
}
POST /v1/flows/{id}/trigger

Start a flow for a contact. The variables you pass become merge tags (like {{location}}) usable in every step of the flow — WhatsApp template variables, SMS bodies, voice TTS scripts. Idempotent on (flowId, phone) within a short window if you re-post with the same body.

Request body

{
  "phone": "+9613197119",
  "variables": {
    "location": "Hamra Street",
    "orderId": "4821",
    "driverName": "Ahmad"
  }
}

cURL

curl -X POST "https://api.cast.wayvzgroup.com/v1/flows/42/trigger" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+9613197119",
    "variables": { "location": "Hamra Street", "orderId": "4821" }
  }'

Response 201 Created

{
  "enrollmentId": 4821,
  "flowId": 42,
  "flowName": "Driver Notification",
  "phone": "+9613197119",
  "status": "active",
  "createdAt": "2026-05-16T10:30:00Z"
}
Use case. A dispatch system posts to this endpoint whenever a driver pickup is scheduled. The flow handles the multi-channel notification (WhatsApp first, voice fallback after 3 minutes, DTMF confirmation) without your code needing to know which channel was used.
GET /v1/flows/enrollments/{id}

Returns the enrollment header + step-by-step execution timeline. Each step shows what ran, when it ran, whether it succeeded, and channel-specific details (DTMF response for voice, delivery status for messages).

cURL

curl "https://api.cast.wayvzgroup.com/v1/flows/enrollments/4821" \
  -H "Authorization: Bearer sk_live_..."

Contacts

POST /v1/contacts

Create or update a contact in one call. Idempotent on phone — safe to re-post the same body. Sets properties and adds the contact to one or more groups (groups are created automatically if they don’t already exist).

Request body

{
  "phone": "+9613197119",
  "firstName": "Ahmad",
  "lastName": "Al-Rashid",
  "email": "ahmad@example.com",
  "properties": {
    "role": "driver",
    "city": "Beirut",
    "vehicle": "motorcycle"
  },
  "groups": ["drivers-beirut"]
}

cURL

curl -X POST "https://api.cast.wayvzgroup.com/v1/contacts" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "phone": "+9613197119", "firstName": "Ahmad", "properties": { "role": "driver" } }'

Response

{
  "phone": "+9613197119",
  "firstName": "Ahmad",
  "lastName": "Al-Rashid",
  "created": false,
  "propertiesSet": 3,
  "groupsAdded": 1
}
GET /v1/contacts

Paginated list of contacts. Optional filters: group (by name), search (matches phone / first / last), page, pageSize (default 50, max 100).

cURL

curl "https://api.cast.wayvzgroup.com/v1/contacts?page=1&pageSize=50&group=drivers-beirut" \
  -H "Authorization: Bearer sk_live_..."
GET /v1/contacts/{phone}

Returns a single contact with all properties, groups, tags, and last-activity timestamp.

cURL

curl "https://api.cast.wayvzgroup.com/v1/contacts/+9613197119" \
  -H "Authorization: Bearer sk_live_..."
POST /v1/campaigns

Send a multi-recipient campaign on a single channel. Supports SMS and WhatsApp via this endpoint; for bulk voice campaigns, use /v1/voice/call per recipient or trigger a flow. Per-recipient variables let you personalize each message.

Request body

{
  "channel": "sms",
  "name": "Driver alert batch",
  "from": "CAST",
  "recipients": ["+9613197119", "+9613197120"],
  "text": "Pickup at {{location}}",
  "variables": {
    "+9613197119": { "location": "Hamra" },
    "+9613197120": { "location": "Verdun" }
  }
}

cURL

curl -X POST "https://api.cast.wayvzgroup.com/v1/campaigns" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "channel": "sms", "name": "Promo", "from": "CAST",
        "recipients": ["+9613197119"], "text": "Hi from Cast" }'

Response 201 Created

{
  "campaignId": "camp_abc12345",
  "recipientCount": 2,
  "estimatedCost": 0.04,
  "status": "queued"
}

Account

GET /v1/ping
Health check. Confirms connectivity and that your key is valid.
{
  "ok": true,
  "account_id": 1234,
  "now": "2026-05-01T11:00:00Z"
}
GET /v1/balance
Current wallet balance. Check this before high-volume sends to avoid mid-campaign failure from an empty wallet.
{
  "balance": 124.50,
  "currency": "USD"
}
GET /v1/account
Account profile, enabled channels, and rate limits.

Webhooks

Cast can POST a JSON event to your URL whenever something happens to a message. Configure endpoints at /developers/webhooks.

Note. Customer-facing webhooks (outbound delivery events to your URL) are on the roadmap. The webhook section below documents inbound Meta status callbacks that Cast processes internally and forwards as structured events.

Event types

EventWhen
message.deliveredCarrier confirmed delivery (WhatsApp).
message.readRecipient opened the message (WhatsApp, when read receipts are enabled).
message.failedCarrier rejected the message; payload includes failureCode and failureReason.
message.inboundA customer replied or sent a new message to your WhatsApp number.

Payload shape

{
  "id": "evt_a1b2c3d4...",
  "type": "message.delivered",
  "created": 1746090000,
  "data": {
    "messageId": "wamid.HBgL...",
    "smsLogId": "12345",
    "to": "9613xxxxxxx",
    "channel": "whatsapp",
    "templateName": "order_shipped",
    "templateLanguage": "en_US",
    "deliveredAt": "2026-05-01T11:00:05Z",
    "readAt": null,
    "failedAt": null,
    "failureCode": null,
    "failureReason": null
  }
}

Signature verification

Every request includes a Cast-Signature header:

Cast-Signature: t=1746090000,v1=<hex>

Where v1 is hex(HMAC_SHA256(secret, t + "." + body)). To verify:

  1. Read the raw request body (do not parse first).
  2. Parse t and v1 from the header.
  3. Reject if t is more than 5 minutes old (replay protection).
  4. Compute hex(HMAC_SHA256(secret, t + "." + body)) and compare to v1 using constant-time comparison.
const crypto = require('crypto');

function verifyCastSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map(kv => kv.split('=')));
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(t + '.' + rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(v1, 'hex'));
}
import hmac, hashlib, time

def verify_cast_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split('=', 1) for p in header.split(','))
    t, v1 = parts.get('t'), parts.get('v1')
    if not t or not v1: return False
    if abs(time.time() - int(t)) > 300: return False

    expected = hmac.new(
        secret.encode(),
        f"{t}.{raw_body.decode()}".encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, v1)
function verifyCastSignature(string $rawBody, string $header, string $secret): bool {
    parse_str(str_replace(',', '&', $header), $parts);
    if (empty($parts['t']) || empty($parts['v1'])) return false;
    if (abs(time() - intval($parts['t'])) > 300) return false;

    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1']);
}

Retry policy

If your endpoint returns anything other than 2xx, Cast retries with exponential backoff:

  • +30 seconds after attempt 1
  • +5 minutes after attempt 2
  • +30 minutes after attempt 3
  • +2 hours after attempt 4
  • +6 hours after attempt 5
  • +12 hours after attempt 6
  • After 7 attempts (~24 hours total) the event is marked dead and removed from the retry queue.

Each request includes Cast-Event-Id, Cast-Event-Type, and Cast-Delivery-Attempt headers for deduplication and tracing.

Idempotency. The same Cast-Event-Id may arrive more than once on retries. Store the event ID and ignore duplicates.

HTTP status codes

200Read/list/upsert succeeded.
201Resource created (message accepted, event inserted).
204No content (CORS preflight response).
400Validation error. Body has error.code.
401Auth missing or invalid.
402Insufficient wallet balance.
403Account or API access disabled.
404Endpoint or resource not found.
405Method not allowed for this endpoint.
413Payload too large (contact properties or event payload).
429Rate-limited (per-second cap exceeded).
500Internal error; safe to retry.

Error codes

HTTPcodeCause & fix
400invalid_jsonBody could not be parsed as JSON. Check syntax and Content-Type: application/json.
400missing_fieldA required field is absent or empty. The message names which field.
400missing_bodyPOST request had an empty body.
400missing_idMessage ID is required for this request.
400invalid_idMessage ID format is invalid.
400invalid_phoneThe phone query parameter is missing or contains fewer than 7 digits.
400invalid_languagelanguage must be 0 (GSM-7) or 1 (Unicode).
400sender_too_longSender ID must be 11 characters or fewer.
400invalid_scheduleschedule must be ISO 8601.
400invalid_property_keyProperty key fails the naming rules (start with letter, letters/digits/dot/underscore/hyphen, max 100 chars).
400no_propertiesAt least one property is required on a POST.
400too_many_propertiesMaximum 50 properties per request.
400invalid_event_typeEvent type must be domain.action format, lowercase, max 50 chars.
400invalid_payloadEvent payload could not be serialized as JSON.
400invalid_occurred_atoccurred_at must be a valid ISO 8601 datetime.
401authentication_requiredMissing Authorization header.
401invalid_api_keyKey not found. Check the key is copied in full.
401api_key_revokedKey was revoked. Generate a new one at /developers.
402insufficient_balanceWallet does not cover the request. Top up at /billing.
403account_inactiveAccount is suspended.
403account_not_foundAccount no longer exists.
403api_access_disabledAPI access is turned off for this account.
404endpoint_not_foundThe resource query parameter does not match any endpoint.
405method_not_allowedWrong HTTP method. The error message states which methods are accepted.
413payload_too_largeProperties body exceeds 16 KB, or event payload exceeds 8 KB.
500internal_errorUnexpected server error. Safe to retry after a short delay.

Changelog

May 2026 (Sprint 7)

  • Added contact-events endpoint — emit custom events that trigger automation flows.
  • Added contact-properties endpoint — store and retrieve per-contact properties for segment conditions.
  • Event-triggered flows: automation flows can now launch when a specific contact event is emitted via API.
  • Segment-triggered flows: flows auto-enroll contacts as they match segment criteria (5-minute evaluation cycle).
  • ROI dashboard available in campaign analytics — tracks cost-per-click and revenue per campaign.

March 2026 (Sprint 5–6)

  • Visual flow builder with 14 node types (WhatsApp, SMS, branch, A/B split, wait, and more).
  • Flow builder supports segment picker and run/pause/resume from the canvas toolbar.
  • Compose page (SMS): multi-source audience combining phones, groups, segments, and CSV with deduplication and live cost estimate.
  • WhatsApp Send: same multi-source audience model as SMS compose.

January 2026 (Sprint 3–4)

  • Initial contact-properties and contact-events API surface defined (Sprint 3a).
  • Flow engine (Sprint 4): send_sms, send_whatsapp, wait, and branch steps fully wired.
  • Inbox V0: conversation-centric WhatsApp inbox with 24-hour service window enforcement and free-text reply.

Need something this page doesn’t cover? Email cast@wayvzgroup.com.