Webhook & Send API
Forward incoming DMs, SMS, and WhatsApp messages to your server the moment they arrive. Send replies back, react to messages, show typing indicators, or hand off conversations to your own system entirely. Supports Facebook, Instagram, SMS, and WhatsApp.
Add-on · $29/month · Requires Creator+ planOverview
The Webhook & Send API add-on gives you programmatic access to the ChatGenius messaging layer. It has two complementary parts:
Webhook Forwarding
When a customer sends you a DM on Facebook or Instagram, an SMS to your provisioned ChatGenius number, or a WhatsApp message to your connected business number, the event is forwarded to your configured HTTPS endpoint in real time.
Send-Reply API
Send DM, SMS, and WhatsApp replies (free-form text or approved templates), react to DM messages, show typing indicators, or unreact â all via a single authenticated POST endpoint using your per-account API key.
You can use these independently or together. A common pattern: receive events via webhook, process them in your own system, and send replies back through the Send-Reply API.
For teams that want to disable ChatGenius AI entirely and handle all replies themselves, see External Reply Mode.
Getting Started
Requirements
- An active ChatGenius subscription on the Creator, Professional, or Business plan
- The Webhook & Send API add-on purchased from your portal (Add to plan â)
- A publicly accessible HTTPS endpoint to receive events (for webhook forwarding)
Portal setup
- Log in to your ChatGenius portal
- Navigate to Integrations in the left sidebar
- Scroll to the Webhook & Send API section
- Enter your HTTPS endpoint URL and select which event types to receive
- Generate your API key (used to authenticate Send-Reply API calls)
- Copy your signing secret (used to verify incoming webhook events)
- Complete the data-sharing attestation and toggle the webhook to Enabled
Your signing secret and API key are always accessible from the portal. You can rotate either at any time without downtime â rotation takes effect immediately and invalidates the previous value.
Webhook Forwarding
When an event occurs, ChatGenius makes a POST request to your configured HTTPS endpoint with a signed JSON payload and a User-Agent: SumGenius-Webhook/1.0 header.
Delivery behavior
- Your endpoint must respond with HTTP 2xx within the configured timeout (default 10 seconds, configurable up to 30 seconds)
- Any non-2xx response or timeout is treated as a failure and retried automatically
- The same X-SumGenius-Delivery-Id (the event's stable unique ID) is used across all retry attempts for the same event
- Events that exhaust all retry attempts are dead-lettered and can be manually retried from the portal
Retry schedule
Failed deliveries are retried on the following fixed schedule (up to 6 attempts total by default):
| Attempt | Delay after previous failure |
|---|---|
| 1st attempt | Immediate |
| 2nd attempt | 30 seconds |
| 3rd attempt | 2 minutes |
| 4th attempt | 10 minutes |
| 5th attempt | 1 hour |
| 6th attempt | 6 hours |
Return 200 OK immediately and process the event asynchronously. This prevents your processing time from counting against the delivery timeout.
Override: if you've enabled Inbound Body Purge, this retry schedule does NOT apply to your inbound SMS and WhatsApp events. Those use exactly one delivery attempt with no retries (so the body can be purged within a tight window). The 6-attempt schedule still applies to your other event types and to clients without the purge toggle on.
Event Types
You can filter which event types get forwarded to your endpoint in the portal. If no event type filters are configured, all supported event types are forwarded.
| Event Type | Description |
|---|---|
| message.received | A customer sent a direct message on Facebook or Instagram |
| message.reel_shared | A customer shared a reel into a direct message thread |
| message.post_shared | A customer shared a regular Instagram post into a direct message thread |
| sms.message_received | A customer sent an SMS to your provisioned ChatGenius number. See SMS Overview. |
| whatsapp.message_received | A customer sent a WhatsApp message to your connected business phone number (text, image, audio, video, document, sticker, location, contacts, or interactive button reply). See WhatsApp Overview. |
The active event type is included in the X-SumGenius-Event request header on every delivery.
Event Payload
Every event is a JSON object posted to your endpoint with Content-Type: application/json.
{
"event_id": "a3f8e1c2d4b567890abcdef01234567",
"event_type": "message.received",
"occurred_at": "2026-02-20T08:15:00Z",
"source": {
"platform": "instagram",
"channel": "meta_dm"
},
"conversation": {
"id": 10482
},
"customer": {
"platform_user_id": "17841400123456789",
"platform_user_name": "Jane Doe",
"platform_user_username": "janedoe"
},
"message": {
"id": "mid.1234567890abcdef",
"text_raw": "Hi, do you have availability next Tuesday?",
"attachments": []
},
"meta": {
"version": "v1",
"payload_profile": "full",
"generated_at": "2026-02-20T08:15:00Z"
}
}
Payload fields
| Field | Type | Description |
|---|---|---|
| event_id | string | Unique identifier for this event. Stays the same across all retry attempts. Use for deduplication. |
| event_type | string | The event type. See Event Types. |
| occurred_at | ISO 8601 | When the event occurred on the platform. |
| source.platform | string | facebook or instagram |
| source.channel | string | Always meta_dm in v1. |
| conversation.id | integer | ChatGenius conversation ID. Pass this to the Send-Reply API to reply. |
| customer.platform_user_id | string | Platform-assigned user ID. Always present. |
| customer.platform_user_name | string|null | Display name. Present in full payload profile only. |
| customer.platform_user_username | string|null | Instagram @handle (without the @ prefix). Present in full payload profile only. null for Facebook conversations. |
| message.id | string | Platform message ID. Use as target_message_id when adding reactions. |
| message.text_raw | string | The raw message text from the customer. |
| message.attachments | array | List of attachments. See attachment fields below. |
| meta.version | string | Payload schema version. Currently v1. |
| meta.payload_profile | string | full or minimal. See Payload Profiles. |
| meta.generated_at | ISO 8601 | When ChatGenius generated the payload. |
Attachment fields
| Field | Profile | Description |
|---|---|---|
| type | both | Attachment category (e.g. image, audio, video) |
| media_type | both | MIME type when available |
| media_id | both | Platform media identifier |
| received_at | both | When the attachment was received |
| url | full only | Media URL (time-limited, provided by Meta) |
| title / caption | full only | Title or caption text when present |
Verifying Signatures
Every webhook delivery includes signature headers so you can confirm it came from ChatGenius and the payload hasn't been tampered with. Always verify the signature before processing an event.
Request headers sent by ChatGenius
| Header | Description |
|---|---|
| X-SumGenius-Signature | HMAC-SHA256 signature of the payload, prefixed with sha256= |
| X-SumGenius-Timestamp | Unix timestamp (seconds) of when the request was signed |
| X-SumGenius-Event | The event type (e.g. message.received) |
| X-SumGenius-Delivery-Id | The event_id. Stays the same across all retry attempts for the same event. |
| User-Agent | SumGenius-Webhook/1.0 |
Signature algorithm
The signed string is the Unix timestamp and the raw JSON request body joined by a period:
signed_string = timestamp + "." + raw_request_body
signature = HMAC-SHA256(signed_string, your_signing_secret)
header_value = "sha256=" + hex(signature)
Compare the computed value to the X-SumGenius-Signature header using a constant-time comparison to prevent timing attacks. Also verify the timestamp is within a reasonable window (we recommend Âą300 seconds) to prevent replay attacks.
// Read the raw body BEFORE json_decode()
$rawBody = file_get_contents('php://input');
$secret = $_ENV['CHATGENIUS_WEBHOOK_SECRET'];
$sigHeader = $_SERVER['HTTP_X_SUMGENIUS_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_SUMGENIUS_TIMESTAMP'] ?? '';
// Reject requests older than 5 minutes
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(400);
exit('Timestamp out of window');
}
$signedString = $timestamp . '.' . $rawBody;
$expected = 'sha256=' . hash_hmac('sha256', $signedString, $secret);
if (!hash_equals($expected, $sigHeader)) {
http_response_code(401);
exit('Invalid signature');
}
// Signature valid â safe to process
$event = json_decode($rawBody, true);
const crypto = require('crypto');
// Use express.raw() to preserve the raw body buffer
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.CHATGENIUS_WEBHOOK_SECRET;
const sigHeader = req.headers['x-sumgenius-signature'] ?? '';
const timestamp = req.headers['x-sumgenius-timestamp'] ?? '';
// Reject requests older than 5 minutes
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).send('Timestamp out of window');
}
const signedString = `${timestamp}.${req.body.toString()}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signedString)
.digest('hex');
const expectedBuf = Buffer.from(expected);
const headerBuf = Buffer.from(sigHeader);
const valid = expectedBuf.length === headerBuf.length
&& crypto.timingSafeEqual(expectedBuf, headerBuf);
if (!valid) return res.status(401).send('Invalid signature');
// Signature valid â acknowledge immediately
res.sendStatus(200);
const event = JSON.parse(req.body.toString());
// process event asynchronously...
});
import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
secret = os.environ['CHATGENIUS_WEBHOOK_SECRET'].encode()
sig_header = request.headers.get('X-SumGenius-Signature', '')
timestamp = request.headers.get('X-SumGenius-Timestamp', '')
# Reject requests older than 5 minutes
if abs(time.time() - float(timestamp)) > 300:
abort(400, 'Timestamp out of window')
raw_body = request.get_data()
signed_string = f"{timestamp}.{raw_body.decode()}".encode()
expected = 'sha256=' + hmac.new(
secret, signed_string, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig_header):
abort(401, 'Invalid signature')
event = request.get_json()
return '', 200
Send-Reply API
Use the Send-Reply API to send messages or reactions from your server back to a customer's conversation on Facebook, Instagram, SMS, or WhatsApp. The same endpoint handles all four platforms â choose the recipient via conversation_id, platform_user_id, or to_phone as appropriate.
Authentication
All Send-Reply API requests must include your API key, generated in the portal under Integrations â Webhook & Send API. Keys are prefixed with sgwh_.
Pass the key using either header:
# Option A â custom header (recommended)
X-SumGenius-Api-Key: sgwh_your_api_key_here
# Option B â standard Bearer token
Authorization: Bearer sgwh_your_api_key_here
Never expose your API key in client-side code or public repositories. Rotate it immediately from the portal if it is ever compromised.
Action: send_message
Send a text message to a customer in an existing conversation. This is the default action when action is omitted.
Request body
| Field | Required | Description |
|---|---|---|
| action | optional | Defaults to send_message when omitted. |
| idempotency_key | required | Your unique key for this request (max 128 chars). Submitting the same key twice returns the original result without resending. |
| conversation_id | required* | The conversation.id from a received webhook event. *Required if platform_user_id and to_phone are not provided. |
| platform_user_id | required* | Platform user ID for Facebook or Instagram (the customer's PSID). *Required if conversation_id is not provided and you're sending to a Meta DM platform. For SMS or WhatsApp, use to_phone instead. |
| to_phone | required* | Recipient phone number in E.164 format (e.g. +15551234567) â SMS or WhatsApp. For WhatsApp also set platform: "whatsapp"; for SMS the platform auto-sets. *Required if conversation_id is not provided. Phone numbers are auto-normalized. |
| platform | optional | facebook, instagram, sms, or whatsapp. Auto-set to sms when to_phone is provided alone. Required for WhatsApp with to_phone; optional when routing by a WhatsApp conversation_id. |
| message_text | required* | The message text to send. Max 1,000 chars on Instagram, 2,000 chars on Facebook, 1,600 chars on SMS (GSM-7) or 1,530 chars (UCS-2), 4,096 chars on WhatsApp. *For WhatsApp templates, use template_name instead â see WhatsApp Templates. See Encoding & Segments for SMS character limits and segmentation rules. |
| template_name | optional | WhatsApp only. Approved template name registered on your WhatsApp Business Account, using Meta's lowercase letters/numbers/underscore format. When provided, sends as a template (any time, no 24h window required). Required for WhatsApp messages outside the customer-care window. See WhatsApp Templates. |
| template_language | optional | WhatsApp only. Template language code (e.g. en_US, es). Defaults to en_US. |
| template_components | optional | WhatsApp only. Array of template components (header, body, button) with parameters. Passed through to Meta's Cloud API verbatim. See WhatsApp Templates. |
curl -X POST https://sumgenius.ai/api/meta/webhook-send.php \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"idempotency_key": "confirm-appt-10482-001",
"conversation_id": 10482,
"message_text": "Your appointment is confirmed for Tuesday at 2pm. See you then!"
}'
const response = await fetch('https://sumgenius.ai/api/meta/webhook-send.php', {
method: 'POST',
headers: {
'X-SumGenius-Api-Key': process.env.CHATGENIUS_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
idempotency_key: 'confirm-appt-10482-001',
conversation_id: 10482,
message_text: 'Your appointment is confirmed for Tuesday at 2pm. See you then!'
})
});
const result = await response.json();
$payload = json_encode([
'idempotency_key' => 'confirm-appt-10482-001',
'conversation_id' => 10482,
'message_text' => 'Your appointment is confirmed for Tuesday at 2pm. See you then!'
]);
$ch = curl_init('https://sumgenius.ai/api/meta/webhook-send.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-SumGenius-Api-Key: ' . getenv('CHATGENIUS_API_KEY'),
'Content-Type: application/json'
]
]);
$result = json_decode(curl_exec($ch), true);
Action: react
Add an emoji reaction to a specific message. Use the message.id from a received webhook event as the target_message_id.
Request body
| Field | Required | Description |
|---|---|---|
| action | required | Must be "react" |
| idempotency_key | required | Your unique key for this request (max 128 chars). |
| conversation_id | required* | ChatGenius conversation ID. *Required if platform_user_id is not provided. |
| platform_user_id | required* | *Required if conversation_id is not provided. |
| platform | optional | facebook or instagram. Required when using platform_user_id without conversation_id. |
| target_message_id | required | The platform message ID to react to (message.id from the event payload). |
| reaction | optional | The reaction emoji name. Defaults to love when omitted. |
Supported reaction values
For action=react, send one of these reaction values in the reaction field:
| Value | Meaning |
|---|---|
| love | â¤ī¸ Love |
| smile | đ Laugh / Smile |
| wow | đŽ Wow |
| sad | đĸ Sad |
| angry | đĄ Angry |
| yes | đ Thumbs up |
| no | đ Thumbs down |
If reaction is omitted for action=react, ChatGenius defaults it to love. For action=unreact, do not send reaction.
curl -X POST https://sumgenius.ai/api/meta/webhook-send.php \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"action": "react",
"idempotency_key": "react-10482-msg-001",
"conversation_id": 10482,
"target_message_id": "mid.1234567890abcdef",
"reaction": "love"
}'
Action: unreact
Remove a previously added reaction from a message.
Request body
| Field | Required | Description |
|---|---|---|
| action | required | Must be "unreact" |
| idempotency_key | required | Your unique key for this request (max 128 chars). |
| conversation_id | required* | ChatGenius conversation ID. *Required if platform_user_id is not provided. |
| platform_user_id | required* | *Required if conversation_id is not provided. |
| platform | optional | facebook or instagram. Required when using platform_user_id without conversation_id. |
| target_message_id | required | The platform message ID to remove the reaction from. |
curl -X POST https://sumgenius.ai/api/meta/webhook-send.php \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"action": "unreact",
"idempotency_key": "unreact-10482-msg-001",
"conversation_id": 10482,
"target_message_id": "mid.1234567890abcdef"
}'
Action: typing_on
Show a typing indicator (the "..." bubble) to the customer. The indicator lasts approximately 20 seconds or until you send a message, whichever comes first. Useful for signaling that your system is processing a reply.
Request body
| Field | Required | Description |
|---|---|---|
| action | required | Must be "typing_on" (alias: "typing") |
| idempotency_key | required | Your unique key for this request (max 128 chars). Use a new key each call â e.g. typing-{conversation_id}-{timestamp}. |
| conversation_id | required* | ChatGenius conversation ID. *Required if platform_user_id is not provided. |
| platform_user_id | required* | *Required if conversation_id is not provided. |
| platform | optional | facebook or instagram. Required when using platform_user_id without conversation_id. |
Typing indicators are ephemeral. They are not retried on failure â if the send fails, the request goes straight to dead_letter. The 7-day messaging window check is also skipped since Meta allows sender_action outside the window.
curl -X POST https://sumgenius.ai/api/meta/webhook-send.php \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"action": "typing_on",
"idempotency_key": "typing-10482-1709500000",
"conversation_id": 10482
}'
Idempotency
Every Send-Reply API request requires an idempotency_key. If you submit the same key again, the original result is returned without resending â making retries safe under network failures.
- Max length: 128 characters
- A duplicate submission returns "status": "duplicate" with the original result
- A reliable key pattern: {your-reference}-{conversation_id}-{sequence}
Response Format
All responses are JSON with Content-Type: application/json.
Success response
{
"success": true,
"status": "sent",
"request_id": "msgreq_...",
"conversation_id": 10482,
"platform": "instagram",
"action": "send_message",
"sent_at": "2026-02-20 08:15:32",
"message": "Message sent successfully."
}
Response fields
| Field | Description |
|---|---|
| success | true on success, false on error |
| status | See status values below |
| request_id | ChatGenius internal request ID. Provide this when contacting support. |
| conversation_id | The resolved conversation ID |
| platform | The resolved platform: facebook or instagram |
| action | The action that was executed |
| target_message_id | The message that was reacted to. Present on react and unreact. |
| reaction | The reaction value applied. Present on react. |
| sent_at | Timestamp string when ChatGenius marked the request as sent. |
Status values
| Status | Description |
|---|---|
| sent | Message was delivered to the platform successfully. |
| queued_retry | Immediate send failed. The request is queued and will be retried automatically. |
| duplicate | This idempotency_key was already processed. No duplicate was sent. |
| dead_letter | All retry attempts exhausted. The message was not delivered. |
SMS Overview
Send and receive SMS messages through the same Webhook & Send API used for Facebook DM, Instagram DM, and WhatsApp. Same authentication, same idempotency, same signing secret, same response shape â just a different event type and an extra routing field (to_phone).
Requirements: SMS access on your tier (Professional or Business) and a provisioned Twilio number under Settings → SMS in the portal. Without a provisioned number, the sms.message_received checkbox on the Webhook & Send API page is disabled.
Inbound vs. outbound
| Direction | How it works | Counts toward SMS limit? |
|---|---|---|
| Inbound (customer texts your number) | Twilio → ChatGenius → HMAC-signed POST to your endpoint with event_type: "sms.message_received" | No â inbound is free |
| Outbound (you send via Send-Reply API) | Your server → ChatGenius → Twilio → recipient. Triggered by action: "send_message" with to_phone | Yes â each successful outbound API call counts as 1 toward your monthly SMS quota, regardless of how many segments Twilio splits the message into |
Pure-relay mode
To handle all SMS conversations yourself with no AI involvement from ChatGenius, you have two options:
- SMS-only: disable SMS AI Assistant under Settings → SMS → AI Assistant. AI stays on for DMs and WhatsApp (if connected); only SMS is pure-relay.
- WhatsApp-only: disable WhatsApp AI Assistant under Settings → WhatsApp → AI Assistant. AI stays on for DMs and SMS (if connected); only WhatsApp is pure-relay.
- All platforms at once: enable Global External Reply Mode on this page. AI is paused across DMs, SMS, and WhatsApp together; conversations auto-claim to your account. See Global External Reply Mode.
With either option enabled for SMS:
- Inbound SMS still hits your endpoint with the full payload — signed, validated, ready to process
- ChatGenius does not generate any AI reply
- You can still send replies via the Send-Reply API (using to_phone or conversation_id)
- The conversation and message history are still recorded in your portal so your team has visibility
Supported actions
SMS supports only the send_message action. The react, unreact, and typing_on actions are Meta-only and return 400 when used with an SMS conversation.
Inbound SMS Payload
When a customer texts your provisioned ChatGenius number, ChatGenius forwards an sms.message_received event to your endpoint with the same envelope as Meta events plus an SMS-specific message.sms sub-block.
{
"event_id": "7f7a80c84bf17c4b5b72974c97365a77",
"event_type": "sms.message_received",
"occurred_at": "2026-04-27T23:22:10Z",
"source": {
"platform": "sms",
"channel": "twilio_sms"
},
"conversation": {
"id": 119301
},
"customer": {
"platform_user_id": "+15551234567",
"phone": "+15551234567"
},
"message": {
"id": "SMdb4342728f73bca6a83770a705e27b31",
"text_raw": "Hi, what time do you open Saturday?",
"attachments": [],
"sms": {
"to_phone": "+15559876543",
"from_phone": "+15551234567",
"num_segments": 1,
"num_media": 0,
"encoding": "GSM-7"
}
},
"meta": {
"version": "v1",
"payload_profile": "full",
"generated_at": "2026-04-27T23:22:10Z"
}
}
SMS-specific fields
| Field | Type | Description |
|---|---|---|
| source.platform | string | Always sms for SMS events |
| source.channel | string | Always twilio_sms for SMS events |
| customer.platform_user_id | string | The customer's phone number in E.164 format. Always present. |
| customer.phone | string | Same as platform_user_id — provided as a convenience for SMS payloads. Present in full payload profile only. |
| message.id | string | The Twilio MessageSid. Stable identifier; use for deduplication. |
| message.text_raw | string | The full SMS body. Multi-segment SMS arrives concatenated — you receive the joined text, not individual segments. |
| message.sms.to_phone | string | Your ChatGenius (Twilio) number that received the SMS, in E.164 format |
| message.sms.from_phone | string | The customer's phone number in E.164 format (same as customer.platform_user_id) |
| message.sms.num_segments | integer | How many SMS segments the inbound message used (informational) |
| message.sms.num_media | integer | The number of media items Twilio reported on the inbound message. The integer count is forwarded for visibility, but MMS media URLs are not included in the v1 payload — message.attachments is always an empty array for SMS in v1. |
| message.sms.encoding | string | GSM-7 or UCS-2 — detected from the message body content |
Signature verification, retry behavior, and timestamp validation are identical to Meta DM events — same X-SumGenius-Signature header, same per-client signing secret, same algorithm. See Verifying Signatures.
Sending SMS via the Send-Reply API
Send an SMS by calling the same Send-Reply endpoint used for DMs, with to_phone instead of conversation_id. The platform is auto-set to sms.
Request body
| Field | Required | Description |
|---|---|---|
| action | optional | Defaults to send_message. SMS supports only this action. |
| idempotency_key | required | Your unique key for this request (max 128 chars). Same key returns "status": "duplicate" — no second SMS sent. |
| to_phone | required* | Recipient phone number in E.164 format (e.g. +15551234567). *Required if conversation_id is not provided. Numbers are auto-normalized — spaces, dashes, parens, and a leading 1 without + are accepted. |
| conversation_id | required* | The conversation.id from a received SMS event. *Use this OR to_phone, not both required. |
| message_text | required | The SMS body. Max 1,600 chars (GSM-7) or 1,530 chars (UCS-2). See Encoding & Segments. |
The sender phone number is automatically set to your client's provisioned sms_phone_number. You cannot specify from_phone in the request — SMS sender selection is server-controlled to enforce A2P 10DLC compliance.
curl -X POST https://sumgenius.ai/api/meta/webhook-send.php \
-H "X-SumGenius-Api-Key: sgwh_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"idempotency_key": "refill-reminder-001",
"action": "send_message",
"to_phone": "+15551234567",
"message_text": "Your prescription is ready for pickup. Reply STOP to opt out."
}'
const response = await fetch('https://sumgenius.ai/api/meta/webhook-send.php', {
method: 'POST',
headers: {
'X-SumGenius-Api-Key': process.env.CHATGENIUS_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
idempotency_key: 'refill-reminder-001',
action: 'send_message',
to_phone: '+15551234567',
message_text: 'Your prescription is ready for pickup. Reply STOP to opt out.'
})
});
const result = await response.json();
console.log(result.status, result.request_id);
$payload = json_encode([
'idempotency_key' => 'refill-reminder-001',
'action' => 'send_message',
'to_phone' => '+15551234567',
'message_text' => 'Your prescription is ready for pickup. Reply STOP to opt out.'
]);
$ch = curl_init('https://sumgenius.ai/api/meta/webhook-send.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-SumGenius-Api-Key: ' . getenv('CHATGENIUS_API_KEY'),
'Content-Type: application/json'
]
]);
$result = json_decode(curl_exec($ch), true);
SMS-specific success response fields
SMS responses follow the same Response Format as DM responses, with these additions:
| Field | Description |
|---|---|
| platform | Always "sms" |
| to_phone | Echoes the recipient phone number (only present when to_phone was sent in the request) |
Encoding & Segments
SMS uses two different encodings depending on what characters are in your message body. ChatGenius detects the encoding automatically and validates against the corresponding cap.
| Encoding | Triggered by | Single segment | Concatenated segment | Hard cap |
|---|---|---|---|---|
| GSM-7 | Latin alphabet, common punctuation, basic European accents | 160 chars | 153 chars | 1,600 chars |
| UCS-2 | Any emoji, non-Latin script (Arabic, Chinese, etc.), or smart quotes | 70 chars | 67 chars | 1,530 chars |
One emoji can drop your character limit by more than half. A 160-character GSM-7 message that previously fit in 1 segment becomes a 70-char-per-segment UCS-2 message the moment a single emoji is added — same text now uses 3 segments. Twilio segments still matter for carrier cost and throughput, but ChatGenius counts each successful outbound Send-Reply API call as 1 SMS quota unit.
Quota counting rules
- Inbound SMS — free, never counted (regardless of segments)
- Outbound SMS — each successful Send-Reply API call counts as 1 toward your monthly quota, regardless of how many segments Twilio splits the message into
- A 200-char GSM-7 message = 1 quota unit (Twilio sends 2 segments; we count 1)
- A 200-char UCS-2 message = 1 quota unit (Twilio sends 3 segments; we count 1)
Segment count still matters for Twilio's own carrier-level throughput limits — sending many long messages quickly can hit Twilio's per-second segment cap before it hits our monthly quota. Keep messages concise where possible.
The detected encoding and segment count are returned in the inbound payload (message.sms.encoding and message.sms.num_segments) so you can monitor cost in real time.
Opt-out handling (STOP / START)
SMS opt-out is handled at multiple layers automatically — you don't need to implement any of this:
- Twilio carrier-level Advanced Opt-Out: when a recipient texts STOP, Twilio blocks all future outbound SMS to that number from your Messaging Service. This is enforced regardless of any application-layer check.
- ChatGenius app-level fast-fail: we mirror the opt-out in our database so future API calls return 410 with error: "Recipient has opted out (replied STOP)." immediately, without consuming a Twilio API call.
- Re-subscription: if the recipient texts START, UNSTOP, or YES, both layers re-enable sending automatically.
You should still include opt-out language in your initial messages (e.g. "Reply STOP to opt out.") to comply with carrier and CASL/TCPA requirements.
SMS Error Reference
SMS errors follow the same general error response shape. The error codes specific to SMS are:
| HTTP | Trigger | Error message |
|---|---|---|
| 400 | to_phone doesn't match E.164 | to_phone must be in E.164 format (e.g. +15551234567). |
| 400 | action is react, unreact, or typing_on with platform sms | action 'react' is not supported for platform 'sms'. Supported: send_message. |
| 403 | SMS not enabled on your account tier | SMS access is not enabled for this account. |
| 410 | Recipient has opted out (replied STOP) | Recipient has opted out (replied STOP). |
| 412 | No Twilio number provisioned for the account | SMS not provisioned for this account. Provision a Twilio number first. |
| 422 | Message exceeds GSM-7 (1,600) or UCS-2 (1,530) cap | message_text exceeds 1600 characters for SMS GSM-7 (1700 provided). |
| 429 | Monthly SMS quota exceeded | Monthly SMS limit reached for this account. |
| 502 | Twilio returned an error (network, invalid number, landline, region not enabled, etc.) | SMS delivery failed: <Twilio error message>. |
| 503 | SMS add-on is temporarily disabled platform-wide | SMS webhook add-on is temporarily disabled. |
Permanent failures (invalid recipient, opted out, over-length) are not retried — they go straight to dead_letter. Transient failures (Twilio 5xx, 429, network errors) are retried with the same exponential backoff schedule used for Meta deliveries.
WhatsApp Overview
ChatGenius routes WhatsApp through Meta's Cloud API directly (not Twilio). Meta is the WhatsApp transport sub-processor on this platform. WhatsApp requires Professional+ tier and a connected WhatsApp business phone number. Once provisioned, both inbound forwarding and outbound Send API are available alongside SMS, Facebook DM, and Instagram DM.
Capabilities
- Inbound forwarding: incoming WhatsApp messages (text, image, audio, video, document, sticker, location, contacts, button replies) forwarded to your endpoint as
whatsapp.message_receivedevents. - Outbound free-form text: reply within the WhatsApp 24-hour customer-care window via
send_messagewithmessage_text. - Outbound templates: approved WhatsApp templates can be sent any time (no 24h window required) via
template_name+template_language+template_components. - Pure-relay mode: set WhatsApp AI Assistant off in your portal â inbound forwarding still fires, no AI auto-reply.
Requirements
- Professional or higher ChatGenius tier (WhatsApp is included)
- Connected WhatsApp Business Account with a verified phone number
- Webhook + Send API add-on active
- The
whatsapp.message_receivedevent filter checked in Configuration
WhatsApp Inbound Payload
Inbound WhatsApp events use the same envelope as Meta DM and SMS, with a WhatsApp-specific sub-block. source.channel is always whatsapp_cloud.
{
"event_id": "f8e7d6c5...",
"event_type": "whatsapp.message_received",
"occurred_at": "2026-05-05T18:30:00+00:00",
"source": {
"platform": "whatsapp",
"channel": "whatsapp_cloud"
},
"conversation": {
"id": 4982
},
"customer": {
"platform_user_id": "15551234567",
"phone": "15551234567",
"profile_name": "Jane Doe"
},
"message": {
"id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBJCRkE5...",
"text_raw": "Yes, please confirm my order",
"attachments": [],
"whatsapp": {
"wa_id": "15551234567",
"phone_number": "+15551234567",
"business_phone_number_id": "108765432109876",
"profile_name": "Jane Doe",
"message_type": "text",
"context_message_id": null
}
},
"meta": {
"version": "v1",
"payload_profile": "full",
"generated_at": "2026-05-05T18:30:00+00:00"
}
}
Field reference
customer.platform_user_id— WhatsAppwa_id(E.164 digits, no leading +).customer.phone— same value, mirrored for parity with SMS.customer.profile_name— WhatsApp profile name from Meta (best-effort).message.id— Metawamid. Use this for idempotent processing on your side.message.whatsapp.business_phone_number_id— the recipient business phone number ID (yours).message.whatsapp.message_type— one oftext,image,audio,video,document,sticker,location,contacts,interactive,button,reaction.message.whatsapp.context_message_id— the wamid of the message this is replying to (button replies, threaded replies); null otherwise.message.attachments— for media-type messages, populated withmedia_id,mime_type,filename, etc. Always an array.
Sending WhatsApp Messages
Use POST /api/meta/webhook-send.php with the same authentication header as other platforms. Set platform: "whatsapp" and to_phone in E.164 format (with or without leading +).
Free-form text (within the 24-hour window)
POST /api/meta/webhook-send.php
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"idempotency_key": "wa_20260505_001",
"action": "send_message",
"platform": "whatsapp",
"to_phone": "+15551234567",
"message_text": "Thanks — your refill is ready for pickup."
}
Free-form text requires the recipient to have messaged you in the last 24 hours (Meta's customer-care window). Outside that window, use a template (next section).
Limits
- Free-form text body max: 4,096 characters.
- Each successful outbound Send API request counts as 1 unit against your platform's outbound counters.
WhatsApp Templates
Approved templates can be sent any time (no 24h window required). Templates must already be registered and approved on your WhatsApp Business Account before use. ChatGenius forwards your template_name + template_language + template_components directly to Meta's Cloud API. Template names must use Meta's lowercase letters/numbers/underscore format, and template_components must be a JSON array.
POST /api/meta/webhook-send.php
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"idempotency_key": "wa_template_20260505_001",
"action": "send_message",
"platform": "whatsapp",
"to_phone": "+15551234567",
"template_name": "order_shipped",
"template_language": "en_US",
"template_components": [
{
"type": "body",
"parameters": [
{"type": "text", "text": "12345"},
{"type": "text", "text": "May 6"}
]
}
]
}
Template component shape
Pass through Meta's Cloud API components array structure as documented in Meta's WhatsApp template documentation. Common component types: header, body, button. Each component contains a parameters array of typed values.
Named parameters vs positional parameters
Meta supports two parameter styles, and the format your template was registered with determines the shape of parameters at send time:
Positional placeholders ({{1}}, {{2}}, ...) — older style, parameters are matched by order:
{
"type": "body",
"parameters": [
{"type": "text", "text": "12345"},
{"type": "text", "text": "May 6"}
]
}
Named placeholders ({{customer_name}}, {{order_number}}, ...) — newer style, each parameter must include a parameter_name field matching the placeholder name from the template definition:
{
"type": "body",
"parameters": [
{"type": "text", "parameter_name": "customer_name", "text": "Jared"},
{"type": "text", "parameter_name": "order_number", "text": "SG4324"},
{"type": "text", "parameter_name": "business_name", "text": "SumGeniusAI"},
{"type": "text", "parameter_name": "order_status", "text": "Shipped"}
]
}
If you mix the two or omit parameter_name for a named-parameter template, Meta returns (#100) Invalid parameter with details "Parameter name is missing or empty." Check your template definition in WhatsApp Manager to see which style it was registered with.
Errors
- (#100) Invalid parameter — usually means the parameter shape doesn't match the template (positional vs named, or missing
parameter_name). - 132001 — Template does not exist (check name and language).
- 132000 — Number of parameters does not match the registered template.
- 132012 — Parameter format does not match (e.g. wrong type for a placeholder).
- 131047 — Re-engagement message error (24h window expired and no template used).
- 132015 — Template paused due to low quality.
Global External Reply Mode
By default, ChatGenius AI/automation can still respond to incoming messages even when webhook forwarding is active. Global External Reply Mode applies a runtime override across all connected platforms — Facebook DM, Instagram DM, SMS, and WhatsApp — so your external system becomes the sole responder.
When to use it
- You have your own AI or support system and want full control over every response on every platform
- You need to route different conversation types to different handlers
- You want ChatGenius purely as a message delivery and forwarding layer
What it does
- Pauses ChatGenius AI/automation replies on FB DM, IG DM, SMS, and WhatsApp — while keeping all inbound forwarding, signing, retries, conversation storage, and the Send-Reply API fully working
- Auto-claims new incoming conversations to your account (across all platforms) so they appear as customer-managed in the portal
- Does not change your saved AI Assistant or Automation toggles in Settings — it's a runtime override that flips off when you disable External Reply Mode
How to enable
Toggle Global External Reply Mode in your portal under Integrations → Webhook & Send API. The change takes effect on the very next inbound message.
Per-platform fine-grained control
If you want AI on for some platforms but off for others (for example, AI for DMs but pure-relay for SMS and WhatsApp), leave Global External Reply Mode off and use each platform's individual AI toggle:
- Facebook / Instagram DMs: AI Assistant toggle in Settings → AI Configuration
- SMS: SMS AI Assistant toggle in Settings → SMS
- WhatsApp: WhatsApp AI Assistant toggle in Settings → WhatsApp
Any individual toggle being off pauses AI for that platform; Global External Reply Mode does the same thing for every platform at once — pause AI, keep forwarding.
When Global External Reply Mode is on, ChatGenius will not auto-respond to any inbound message. Make sure your webhook endpoint is live and your external system is ready to handle every conversation before flipping this on.
Inbound Body Purge After Forward
An optional per-account toggle for compliance scenarios where your server is the system of record and ChatGenius should not retain message bodies after forwarding. Designed for healthcare-adjacent and privacy-strict deployments.
Pure-relay only â incompatible with ChatGenius AI. This toggle clears the conversation context that AI uses to compose replies (recent message history, batched user inputs, follow-up scheduling state, active flow variables). With AI Assistant on, enabling this would cause incomplete or broken replies. Required configuration: AI Assistant off for SMS and WhatsApp individually, OR Global External Reply Mode on (which pauses AI across every connected platform). Use this only when your server handles every reply.
What it does
When enabled, after we forward an inbound SMS or WhatsApp message to your endpoint, we scrub:
- The message body text from
meta_messages.message_content - The forwarded JSON payload's
message.text_rawandmessage.attachmentsfields - For SMS: the queue entries in
sms_webhook_queue.message_bodyandsms_webhook_queue.webhook_data - For WhatsApp: the
meta_messages.attachmentsJSON metadata and any downloaded media files on disk
What we retain
For audit and routing purposes:
- Message ID (Twilio MessageSid for SMS, Meta wamid for WhatsApp)
- Timestamp
- Sender identifier (E.164 phone number)
- Direction (inbound)
- Delivery status (delivered, failed, etc.)
- Ticket reference — if your endpoint returns a top-level
ticket_refstring in its 2xx response body, we capture it. If missing or malformed, we still purge â the 2xx status is the only purge trigger.
One-shot delivery semantics
When the toggle is on, the forwarder uses max_attempts = 1 for SMS and WhatsApp inbound events. We make exactly one HTTP attempt to your endpoint (with a 10-second timeout) and then purge the body regardless of outcome — success or failure. This keeps the in-DB lifetime tight for compliance.
Inbox impact
SMS and WhatsApp threads will appear in the ChatGenius unified inbox as metadata-only entries: message ID, timestamp, sender, direction, delivery status, ticket reference. The message body is not displayed (because it's not stored). The inbox itself remains active so you can connect other channels (Facebook DM, Instagram DM) without reconfiguration.
Outbound message retention
Outbound message bodies (messages your server sends through us) are retained for 90 days for delivery troubleshooting, then automatically cleared for accounts using this compliance mode.
Backups
The nightly backup pipeline uses sanitized shadow tables for the three body-bearing tables before the dump is compressed and replicated. For opted-in accounts, inbound SMS and WhatsApp bodies are removed from backup data even if the live forward attempt is still pending when the snapshot starts. Metadata remains backup-recoverable, including message IDs, timestamps, sender identifiers, delivery state, and ticket references. Backup retention is 14 days local + 14 days on Cloudflare R2.
Enabling it
Before flipping the toggle on, disable AI and Automation for the affected platforms â either:
- Per platform: turn off SMS AI Assistant and SMS Automation in Settings â SMS, and turn off WhatsApp AI Assistant and WhatsApp Automation in Settings â WhatsApp.
- Or globally: enable Global External Reply Mode on the Send API tab.
Then in Integrations â Webhook & Send API â Send API tab, flip Purge inbound message body after successful forward (SMS + WhatsApp). Owner-only. Default off. The change applies on the next inbound message.
Why these dependencies
The purger scrubs inbound bodies from meta_messages, the webhook event queue, and the SMS staging table. ChatGenius AI normally pulls conversation history from those exact tables to compose replies, batch rapid-fire user inputs, schedule follow-ups, and drive flow logic. If the purger fires while AI is also running on the same conversation, AI's working memory disappears mid-stream and replies become incomplete or wrong. Disabling AI on the affected platforms (or globally) ensures AI never tries to use what's about to be purged.
Once a body has been purged from a delivered or dead-letter event, you cannot redeliver that event from ChatGenius. The Retry buttons in the Delivery Log are disabled for purged events. Make sure your endpoint persists what it needs before responding 2xx.
Payload Profiles
Choose how much customer data is included in forwarded events. Configure your profile in the portal under Integrations â Webhook & Send API.
| Profile | Included fields | Use when |
|---|---|---|
| full | All fields â including customer.platform_user_name, customer.platform_user_username, attachment URLs, and captions | You need the full message context to display or process conversations |
| minimal | Core fields only â no display names, no attachment URLs or captions | You only need IDs and raw message text, or want to minimize data transmitted |
The active profile is reflected in meta.payload_profile on every event.
Error Reference
Errors always return "success": false with an error string describing what went wrong.
{
"success": false,
"status": "error",
"error": "message_text is required for action=send_message."
}
HTTP status codes
Limits
Message length
| Platform | Max characters | Notes |
|---|---|---|
| 1,000 | â | |
| 2,000 | â | |
| sms (GSM-7) | 1,600 | Auto-split into 160-char segments |
| sms (UCS-2) | 1,530 | Triggered by emoji or non-Latin characters; segments are 70 chars |
Webhook delivery
| Parameter | Default | Range |
|---|---|---|
| Endpoint timeout | 10 seconds | 3 â 30 seconds |
| Max retry attempts | 6 | 1 â 10 |
| Idempotency key length | â | 1 â 128 characters |
| Endpoint URL length | â | Max 2,048 characters |
Endpoint URLs must use HTTPS and resolve to a public IP address. Private IP ranges and localhost are not accepted.
Platform rate limits
Outbound DM messages go through Meta's Messenger API. SMS goes through Twilio. Each provider enforces rate limits per connected account:
| Platform | Limit |
|---|---|
| Instagram DM | 100 messages/second, 750 private replies/hour |
| Facebook Messenger | 300 messages/second, 750 private replies/hour |
| SMS (Twilio) | Subject to Twilio's A2P 10DLC throughput tier for your Messaging Service. New US brands typically start at the low-volume tier (~4,500 segments/day, ~1–3 segments/second); higher volumes require additional Twilio brand vetting. A2P 10DLC registration is applied automatically — your provisioned number is registered under our brand at provisioning time, no extra setup required from you. |
If a provider returns a rate limit error, the request is automatically queued for retry using the schedule above.