Complete MVP implementation of the Cast GHL Conversation Provider bridge: - Go module setup with chi router and mongo-driver dependencies - Config loading with env var validation and defaults - MongoDB token store with upsert, get, update, delete operations - Cast.ph SMS client with 429 retry logic and typed errors - Phone number normalization (E.164 ↔ Philippine local format) - GHL OAuth 2.0 install/callback/refresh flow - GHL webhook handler with ECDSA signature verification (async dispatch) - GHL API client for message status updates and inbound message stubs - Multi-stage Dockerfile, docker-compose with MongoDB, Woodpecker CI pipeline - Unit tests for phone normalization, Cast client, GHL webhook, and OAuth handlers Co-Authored-By: SideKx <sidekx.ai@sds.dev>
13 KiB
Cast SMS API — Developer Documentation
Base URL: https://api.cast.ph
API Version: v1
Overview
The Cast API allows you to send SMS, OTP, and SIM messages programmatically. All API requests must be authenticated using an API key provided by Cast.
Authentication
All requests to the SMS and OTP endpoints require an API key passed in the request header.
Header:
X-API-Key: cast_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API keys have the format cast_ followed by 64 hexadecimal characters. Keep your API key secret — do not expose it in client-side code or public repositories.
Endpoints
1. Send SMS
POST /api/sms/send
Sends an SMS message to the specified destination number.
Request Headers
| Header | Required | Value |
|---|---|---|
X-API-Key |
Yes | Your API key |
Content-Type |
Yes | application/json |
Request Body
{
"to": "09171234567",
"message": "Hello, this is a test message.",
"sender_id": "CAST"
}
| Field | Type | Required | Description |
|---|---|---|---|
to |
string | Yes | Recipient phone number. Must be an 11-digit Philippine mobile number (e.g. 09171234567). |
message |
string | Yes | Message content. Max 300 characters (2 SMS parts). Absolute limit is 450 characters (3 SMS parts). |
sender_id |
string | No | Sender ID to use. Max 11 characters. Must be pre-approved. Defaults to your assigned sender ID if only one is configured. |
Success Response — 200 OK
{
"success": true,
"message_id": "abc123def456",
"parts": 1
}
| Field | Type | Description |
|---|---|---|
success |
bool | true if the message was sent. |
message_id |
string | Gateway-assigned message identifier. |
parts |
int | Number of SMS parts (standard SMS = 1 part per 160 characters). |
2. Send Bulk SMS
POST /api/sms/bulk
Sends the same SMS message to multiple recipients in a single API call. Up to 1,000 destinations per request.
Credits are checked upfront for the full batch before any messages are sent. Each destination is sent individually and logged separately, so partial success is possible if a send fails mid-batch.
Request Headers
| Header | Required | Value |
|---|---|---|
X-API-Key |
Yes | Your API key |
Content-Type |
Yes | application/json |
Request Body
{
"to": ["09171234567", "09181234567", "09191234567"],
"message": "Hello, this is a broadcast message.",
"sender_id": "CAST"
}
| Field | Type | Required | Description |
|---|---|---|---|
to |
array of strings | Yes | List of recipient phone numbers. Min 1, max 1,000. Each must be a valid phone number. |
message |
string | Yes | Message content to send to all recipients. |
sender_id |
string | No | Sender ID to use. Must be pre-approved. Defaults to your assigned sender ID if only one is configured. |
Success Response — 200 OK
{
"success": true,
"total": 3,
"sent": 3,
"failed": 0,
"results": [
{ "to": "09171234567", "success": true, "message_id": "abc123", "parts": 1 },
{ "to": "09181234567", "success": true, "message_id": "def456", "parts": 1 },
{ "to": "09191234567", "success": true, "message_id": "ghi789", "parts": 1 }
]
}
| Field | Type | Description |
|---|---|---|
success |
bool | true only if all destinations were sent successfully. |
total |
int | Total number of destinations in the request. |
sent |
int | Number of messages successfully sent. |
failed |
int | Number of messages that failed. |
results |
array | Per-destination result. Each entry includes to, success, and on success: message_id and parts. On failure: error. |
Note: The HTTP status is always 200 when the request itself is valid, even if some individual sends failed. Check success and failed in the response body to detect partial failures.
Error Response (request-level) — 402, 400, 403
Request-level errors (invalid input, insufficient credits, bad sender ID) return a flat error response — no results array:
{
"success": false,
"error": "insufficient credits: need 6, have 3"
}
3. Send OTP
POST /api/otp/send
Sends an OTP (One-Time Password) message. Identical to the SMS endpoint but routed through a dedicated OTP gateway.
Request Headers
Same as Send SMS.
Request Body
Same as Send SMS.
Success Response
Same as Send SMS.
4. Send SIM
POST /api/sim/send
Sends a message via a SIM gateway (SMPP-to-SIM). Same request and response shape as Send SMS, but routed through the SIM pool.
Account requirement: The
simchannel must be explicitly enabled on your account. Accounts default to SMS and OTP only. Contact Cast to enable SIM access.
Request Headers
Same as Send SMS.
Request Body
Same as Send SMS.
Success Response
Same as Send SMS.
Additional Error Responses
| Status | Error | Cause |
|---|---|---|
403 Forbidden |
"sim channel not enabled for this account" |
Your account does not have SIM access |
503 Service Unavailable |
"sim service is disabled" |
The SIM gateway is not configured on the platform |
5. Send Bulk SIM
POST /api/sim/bulk
Sends the same message to multiple recipients via the SIM gateway. Identical to Send Bulk SMS in behavior and response shape, but routed through the SIM pool.
Account requirement: Same as Send SIM — the
simchannel must be enabled on your account.
Request Headers
Same as Send SMS.
Request Body
Same as Send Bulk SMS.
Success Response
Same as Send Bulk SMS.
Additional Error Responses
| Status | Error | Cause |
|---|---|---|
403 Forbidden |
"sim channel not enabled for this account" |
Your account does not have SIM access |
503 Service Unavailable |
"sim service is disabled" |
The SIM gateway is not configured on the platform |
Error Handling
All errors return a JSON body in this format:
{
"success": false,
"error": "error message here"
}
HTTP Status Codes
| Status Code | Meaning |
|---|---|
200 OK |
Request succeeded |
400 Bad Request |
Invalid or missing request fields |
401 Unauthorized |
Missing or invalid API key |
402 Payment Required |
Insufficient credits |
403 Forbidden |
Access denied (see below) |
429 Too Many Requests |
Rate limit exceeded |
500 Internal Server Error |
Unexpected server error |
503 Service Unavailable |
SMS/SIM service is temporarily disabled |
Common Error Messages
| Error Message | Status | Cause |
|---|---|---|
"missing X-API-Key header" |
401 | No API key provided |
"invalid api key" |
401 | API key not found or invalid |
"api key is revoked" |
403 | API key has been deactivated |
"api key has expired" |
403 | API key passed its expiration date |
"user account is inactive" |
403 | Account has been disabled |
"ip not whitelisted" |
403 | Request origin IP is not in the allowed list |
"sim channel not enabled for this account" |
403 | SIM access is not enabled on your account |
"insufficient credits: need X, have Y" |
402 | Not enough credits to send the message |
"no sender IDs assigned to this user" |
403 | No sender ID has been configured for your account |
"sender_id 'X' is not allowed for this user" |
403 | The specified sender ID is not approved for your account |
"to is required" |
400 | The to field is missing |
"to must be 7-15 characters" |
400 | Phone number is not a valid length |
"message is required" |
400 | The message field is missing |
"message is too long (max 450 characters)" |
400 | Message exceeds the 3-part SMS limit |
"sender ID is too long (max 11 characters)" |
400 | Sender ID exceeds the character limit |
Rate Limits
| Limit | Value |
|---|---|
| Requests per second | 30 |
| Burst (max simultaneous) | 50 |
When you exceed the rate limit, the API returns 429 Too Many Requests with a Retry-After: 60 header. Wait the specified number of seconds before retrying.
SMS Parts & Credits
SMS messages are divided into parts based on length:
| Encoding | Single SMS | Multi-part SMS (per part) |
|---|---|---|
| GSM-7 (standard characters) | 160 chars | 153 chars |
| Unicode (special characters, emoji) | 70 chars | 67 chars |
Credits are deducted per part after the message is successfully sent. For example, a 200-character standard message uses 2 parts and costs 2 credits.
IP Whitelisting
If your account has IP whitelisting enabled, only requests from approved IP addresses or CIDR ranges will be accepted. Requests from unlisted IPs will receive a 403 Forbidden response.
Contact Cast to add or update your whitelisted IPs.
Sender ID
A Sender ID is the name or number displayed as the sender on the recipient's device (e.g., MYAPP, BANK, 639171234567).
- Sender IDs must be pre-approved before use.
- If your account has only one Sender ID, the
sender_idfield in the request is optional — it will be used automatically. - If your account has multiple Sender IDs, you must specify the
sender_idin each request.
Contact Cast to register a Sender ID.
Code Examples
cURL — Bulk SMS
curl -X POST https://api.cast.ph/api/sms/bulk \
-H "X-API-Key: cast_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"to": ["09171234567", "09181234567"],
"message": "Hello from Cast!",
"sender_id": "MYAPP"
}'
cURL — Single SMS
curl -X POST https://api.cast.ph/api/sms/send \
-H "X-API-Key: cast_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"to": "09171234567",
"message": "Your verification code is 123456.",
"sender_id": "MYAPP"
}'
JavaScript (fetch)
const response = await fetch("https://api.cast.ph/api/sms/send", {
method: "POST",
headers: {
"X-API-Key": "cast_your_api_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "639171234567",
message: "Your verification code is 123456.",
sender_id: "MYAPP",
}),
});
const data = await response.json();
if (data.success) {
console.log("Sent! Message ID:", data.message_id, "Parts:", data.parts);
} else {
console.error("Failed:", data.error);
}
Python (requests)
import requests
response = requests.post(
"https://api.cast.ph/api/sms/send",
headers={
"X-API-Key": "cast_your_api_key_here",
"Content-Type": "application/json",
},
json={
"to": "09171234567",
"message": "Your verification code is 123456.",
"sender_id": "MYAPP",
},
)
data = response.json()
if data["success"]:
print(f"Sent! Message ID: {data['message_id']}, Parts: {data['parts']}")
else:
print(f"Failed: {data['error']}")
PHP (cURL)
<?php
$apiKey = "cast_your_api_key_here";
$payload = json_encode([
"to" => "639171234567",
"message" => "Your verification code is 123456.",
"sender_id" => "MYAPP",
]);
$ch = curl_init("https://api.cast.ph/api/sms/send");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: $apiKey",
"Content-Type: application/json",
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($response["success"]) {
echo "Sent! Message ID: " . $response["message_id"] . "\n";
} else {
echo "Failed: " . $response["error"] . "\n";
}
C# (HttpClient)
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "cast_your_api_key_here");
var payload = JsonSerializer.Serialize(new
{
to = "09171234567",
message = "Your verification code is 123456.",
sender_id = "MYAPP"
});
var content = new StringContent(payload, Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.cast.ph/api/sms/send", content);
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var root = doc.RootElement;
if (root.GetProperty("success").GetBoolean())
{
Console.WriteLine($"Sent! Message ID: {root.GetProperty("message_id").GetString()}, Parts: {root.GetProperty("parts").GetInt32()}");
}
else
{
Console.WriteLine($"Failed: {root.GetProperty("error").GetString()}");
}
Support
For API access, sender ID registration, IP whitelisting, or any technical issues, contact the Cast team.