cast-ghl-plugin/CAST_API_REFERENCE.md
Head of Product & Engineering a40a4aa626
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: initial implementation of Cast GHL Provider
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>
2026-04-04 17:27:05 +02:00

464 lines
13 KiB
Markdown

# 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
```json
{
"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`
```json
{
"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
```json
{
"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`
```json
{
"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:
```json
{
"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](#1-send-sms).
#### Request Body
Same as [Send SMS](#1-send-sms).
#### Success Response
Same as [Send SMS](#1-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 `sim` channel 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](#1-send-sms).
#### Request Body
Same as [Send SMS](#1-send-sms).
#### Success Response
Same as [Send SMS](#1-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](#2-send-bulk-sms) in behavior and response shape, but routed through the SIM pool.
> **Account requirement:** Same as [Send SIM](#4-send-sim) — the `sim` channel must be enabled on your account.
#### Request Headers
Same as [Send SMS](#1-send-sms).
#### Request Body
Same as [Send Bulk SMS](#2-send-bulk-sms).
#### Success Response
Same as [Send Bulk SMS](#2-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:
```json
{
"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_id` field in the request is optional — it will be used automatically.
- If your account has multiple Sender IDs, you **must** specify the `sender_id` in each request.
Contact Cast to register a Sender ID.
---
## Code Examples
### cURL — Bulk SMS
```bash
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
```bash
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)
```js
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)
```python
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
<?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)
```csharp
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.