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
| Method | Path | What it does |
GET | /v1/ping | Health check + auth test |
GET | /v1/balance | Current wallet balance |
GET | /v1/account | Account profile + rate limits |
POST | /v1/messages | Send SMS (single or bulk) |
GET | /v1/messages | List recent messages |
GET | /v1/messages/{id} | Get a single message by ID |
POST | /v1/whatsapp/messages | Send a WhatsApp template message |
POST | /v1/voice/call | Place a voice call (TTS + DTMF) |
GET | /v1/voice/call/{callSid} | Voice call status + duration + DTMF |
GET | /v1/flows | List your flows + 24h enrollment counts |
POST | /v1/flows/{id}/trigger | Start a flow for a contact (with merge-tag variables) |
GET | /v1/flows/enrollments/{id} | Enrollment detail with step-by-step timeline |
POST | /v1/contacts | Create or update a contact (idempotent on phone) |
GET | /v1/contacts | List contacts (paginated, filter by group + search) |
GET | /v1/contacts/{phone} | Get contact with properties + groups + tags |
GET | /v1/contacts/{phone}/properties | List a contact’s custom properties |
POST | /v1/contacts/{phone}/properties | Upsert one or more properties |
GET | /v1/contacts/{phone}/events | List recent events for a contact |
POST | /v1/contacts/{phone}/events | Emit a custom event |
POST | /v1/links | Create a slnk.ai short link |
POST | /v1/campaigns | Send 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
| Field | Type | | Notes |
to | string | string[] | required | E.164 number(s). Plus sign optional. Array for bulk (max 1,000). |
from | string | required | Sender ID — up to 11 chars. Alphanumeric senders need per-country registration. |
text | string | required | Message body. |
language | int | optional | 0 = GSM-7 (default, 160 chars/segment), 1 = Unicode (70 chars/segment). |
schedule | string | optional | ISO 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
limit | int | 1–200, default 50. |
offset | int | Offset for pagination. |
to | string | Filter by recipient phone number. |
status | string | queued, sent, delivered, read, failed. |
from_date / to_date | string | ISO 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
| Field | Type | | Notes |
to | string | required | E.164 phone number, with or without leading +. |
template_name | string | required | Exactly as shown in your approved templates list. |
template_language | string | required | e.g. en_US, ar. |
template_category | string | required | UTILITY, MARKETING, or AUTHENTICATION. Affects per-message pricing. |
template_params | string[] | optional | Body 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.
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_..."
Links & Campaigns
POST
/v1/links
Mint a slnk.ai short link. If slug is omitted, Cast generates a random
6-character token. Conflicts auto-suffix with 4 random chars.
Request body
{
"url": "https://cast.wayvzgroup.com/p/toters-promo",
"slug": "toters-may",
"utmSource": "api",
"utmMedium": "sms"
}
cURL
curl -X POST "https://api.cast.wayvzgroup.com/v1/links" \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/landing", "slug": "promo-may" }'
Response 201 Created
{
"shortUrl": "https://slnk.ai/toters-may",
"slug": "toters-may",
"destinationUrl": "https://cast.wayvzgroup.com/p/toters-promo",
"createdAt": "2026-05-16T10:30:00Z"
}
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
| Event | When |
message.delivered | Carrier confirmed delivery (WhatsApp). |
message.read | Recipient opened the message (WhatsApp, when read receipts are enabled). |
message.failed | Carrier rejected the message; payload includes failureCode and failureReason. |
message.inbound | A 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:
- Read the raw request body (do not parse first).
- Parse
t and v1 from the header.
- Reject if
t is more than 5 minutes old (replay protection).
- 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
200 | Read/list/upsert succeeded. |
201 | Resource created (message accepted, event inserted). |
204 | No content (CORS preflight response). |
400 | Validation error. Body has error.code. |
401 | Auth missing or invalid. |
402 | Insufficient wallet balance. |
403 | Account or API access disabled. |
404 | Endpoint or resource not found. |
405 | Method not allowed for this endpoint. |
413 | Payload too large (contact properties or event payload). |
429 | Rate-limited (per-second cap exceeded). |
500 | Internal error; safe to retry. |
Error codes
| HTTP | code | Cause & fix |
| 400 | invalid_json | Body could not be parsed as JSON. Check syntax and Content-Type: application/json. |
| 400 | missing_field | A required field is absent or empty. The message names which field. |
| 400 | missing_body | POST request had an empty body. |
| 400 | missing_id | Message ID is required for this request. |
| 400 | invalid_id | Message ID format is invalid. |
| 400 | invalid_phone | The phone query parameter is missing or contains fewer than 7 digits. |
| 400 | invalid_language | language must be 0 (GSM-7) or 1 (Unicode). |
| 400 | sender_too_long | Sender ID must be 11 characters or fewer. |
| 400 | invalid_schedule | schedule must be ISO 8601. |
| 400 | invalid_property_key | Property key fails the naming rules (start with letter, letters/digits/dot/underscore/hyphen, max 100 chars). |
| 400 | no_properties | At least one property is required on a POST. |
| 400 | too_many_properties | Maximum 50 properties per request. |
| 400 | invalid_event_type | Event type must be domain.action format, lowercase, max 50 chars. |
| 400 | invalid_payload | Event payload could not be serialized as JSON. |
| 400 | invalid_occurred_at | occurred_at must be a valid ISO 8601 datetime. |
| 401 | authentication_required | Missing Authorization header. |
| 401 | invalid_api_key | Key not found. Check the key is copied in full. |
| 401 | api_key_revoked | Key was revoked. Generate a new one at /developers. |
| 402 | insufficient_balance | Wallet does not cover the request. Top up at /billing. |
| 403 | account_inactive | Account is suspended. |
| 403 | account_not_found | Account no longer exists. |
| 403 | api_access_disabled | API access is turned off for this account. |
| 404 | endpoint_not_found | The resource query parameter does not match any endpoint. |
| 405 | method_not_allowed | Wrong HTTP method. The error message states which methods are accepted. |
| 413 | payload_too_large | Properties body exceeds 16 KB, or event payload exceeds 8 KB. |
| 500 | internal_error | Unexpected 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.