Webhook & Send API
Forward incoming DMs 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.
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, ChatGenius forwards the event to your configured HTTPS endpoint in real time.
Send-Reply API
Send messages back, react to 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.
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 |
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 or Instagram.
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 is not provided. |
| platform_user_id | required* | Platform user ID. *Required if conversation_id is not provided. |
| platform | optional | facebook or instagram. Required when using platform_user_id without conversation_id. |
| message_text | required | The message text to send. Max 1,000 chars on Instagram, 2,000 chars on Facebook. |
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. |
External Reply Mode
By default, ChatGenius AI/automation can still respond to incoming DMs even when webhook forwarding is active. External Reply Mode applies a runtime override for Facebook/Instagram DMs so your external system becomes the sole responder for those DM conversations.
When to use it
- You have your own AI or support system and want full control over every response
- You need to route different conversation types to different handlers
- You want ChatGenius purely as a message delivery and forwarding layer
How to enable
Toggle External Reply Mode in your portal under Integrations → Webhook & Send API. The change takes effect immediately.
When External Reply Mode is on, ChatGenius DM AI/automation replies are paused for Facebook/Instagram DMs. Keep your webhook endpoint live before enabling it.
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 |
|---|---|
| 1,000 | |
| 2,000 |
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 messages go through Meta's Messenger API. Meta 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 |
If Meta returns a rate limit error, the request is automatically queued for retry using the schedule above.