ChatGenius Pricing About Blog Login
ChatGenius Developer Docs

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+ plan
Export this page as a .md file for your LLM workflow.

Overview

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

  1. Log in to your ChatGenius portal
  2. Navigate to Integrations in the left sidebar
  3. Scroll to the Webhook & Send API section
  4. Enter your HTTPS endpoint URL and select which event types to receive
  5. Generate your API key (used to authenticate Send-Reply API calls)
  6. Copy your signing secret (used to verify incoming webhook events)
  7. 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):

AttemptDelay after previous failure
1st attemptImmediate
2nd attempt30 seconds
3rd attempt2 minutes
4th attempt10 minutes
5th attempt1 hour
6th attempt6 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 TypeDescription
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.

JSON — message.received example
{
  "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

FieldTypeDescription
event_idstringUnique identifier for this event. Stays the same across all retry attempts. Use for deduplication.
event_typestringThe event type. See Event Types.
occurred_atISO 8601When the event occurred on the platform.
source.platformstringfacebook or instagram
source.channelstringAlways meta_dm in v1.
conversation.idintegerChatGenius conversation ID. Pass this to the Send-Reply API to reply.
customer.platform_user_idstringPlatform-assigned user ID. Always present.
customer.platform_user_namestring|nullDisplay name. Present in full payload profile only.
customer.platform_user_usernamestring|nullInstagram @handle (without the @ prefix). Present in full payload profile only. null for Facebook conversations.
message.idstringPlatform message ID. Use as target_message_id when adding reactions.
message.text_rawstringThe raw message text from the customer.
message.attachmentsarrayList of attachments. See attachment fields below.
meta.versionstringPayload schema version. Currently v1.
meta.payload_profilestringfull or minimal. See Payload Profiles.
meta.generated_atISO 8601When ChatGenius generated the payload.

Attachment fields

FieldProfileDescription
typebothAttachment category (e.g. image, audio, video)
media_typebothMIME type when available
media_idbothPlatform media identifier
received_atbothWhen the attachment was received
urlfull onlyMedia URL (time-limited, provided by Meta)
title / captionfull onlyTitle 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

HeaderDescription
X-SumGenius-SignatureHMAC-SHA256 signature of the payload, prefixed with sha256=
X-SumGenius-TimestampUnix timestamp (seconds) of when the request was signed
X-SumGenius-EventThe event type (e.g. message.received)
X-SumGenius-Delivery-IdThe event_id. Stays the same across all retry attempts for the same event.
User-AgentSumGenius-Webhook/1.0

Signature algorithm

The signed string is the Unix timestamp and the raw JSON request body joined by a period:

Plaintext
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.

PHP
// 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);
Node.js (Express)
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...
});
Python (Flask)
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.

POST /api/meta/webhook-send.php

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:

HTTP headers
# 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

FieldRequiredDescription
actionoptionalDefaults to send_message when omitted.
idempotency_keyrequiredYour unique key for this request (max 128 chars). Submitting the same key twice returns the original result without resending.
conversation_idrequired*The conversation.id from a received webhook event. *Required if platform_user_id is not provided.
platform_user_idrequired*Platform user ID. *Required if conversation_id is not provided.
platformoptionalfacebook or instagram. Required when using platform_user_id without conversation_id.
message_textrequiredThe message text to send. Max 1,000 chars on Instagram, 2,000 chars on Facebook.
curl
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!"
  }'
Node.js
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();
PHP
$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

FieldRequiredDescription
actionrequiredMust be "react"
idempotency_keyrequiredYour unique key for this request (max 128 chars).
conversation_idrequired*ChatGenius conversation ID. *Required if platform_user_id is not provided.
platform_user_idrequired**Required if conversation_id is not provided.
platformoptionalfacebook or instagram. Required when using platform_user_id without conversation_id.
target_message_idrequiredThe platform message ID to react to (message.id from the event payload).
reactionoptionalThe reaction emoji name. Defaults to love when omitted.

Supported reaction values

For action=react, send one of these reaction values in the reaction field:

ValueMeaning
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
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

FieldRequiredDescription
actionrequiredMust be "unreact"
idempotency_keyrequiredYour unique key for this request (max 128 chars).
conversation_idrequired*ChatGenius conversation ID. *Required if platform_user_id is not provided.
platform_user_idrequired**Required if conversation_id is not provided.
platformoptionalfacebook or instagram. Required when using platform_user_id without conversation_id.
target_message_idrequiredThe platform message ID to remove the reaction from.
curl
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

FieldRequiredDescription
actionrequiredMust be "typing_on" (alias: "typing")
idempotency_keyrequiredYour unique key for this request (max 128 chars). Use a new key each call — e.g. typing-{conversation_id}-{timestamp}.
conversation_idrequired*ChatGenius conversation ID. *Required if platform_user_id is not provided.
platform_user_idrequired**Required if conversation_id is not provided.
platformoptionalfacebook 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
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

JSON
{
  "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

FieldDescription
successtrue on success, false on error
statusSee status values below
request_idChatGenius internal request ID. Provide this when contacting support.
conversation_idThe resolved conversation ID
platformThe resolved platform: facebook or instagram
actionThe action that was executed
target_message_idThe message that was reacted to. Present on react and unreact.
reactionThe reaction value applied. Present on react.
sent_atTimestamp string when ChatGenius marked the request as sent.

Status values

StatusDescription
sentMessage was delivered to the platform successfully.
queued_retryImmediate send failed. The request is queued and will be retried automatically.
duplicateThis idempotency_key was already processed. No duplicate was sent.
dead_letterAll 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.

ProfileIncluded fieldsUse 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.

JSON — error response
{
  "success": false,
  "status":  "error",
  "error":   "message_text is required for action=send_message."
}

HTTP status codes

200Request accepted. Always check the status field — a 200 may also indicate queued_retry or duplicate.
400Invalid or missing request fields, or non-JSON body.
401Missing or invalid API key.
403Valid API key, but the webhook add-on is not active on this account.
405Wrong HTTP method — only POST is accepted.
422Validation passed but the request cannot be fulfilled (e.g. message too long for platform, conversation not found).
500Unexpected server error. If this persists, contact support with your request_id.

Limits

Message length

PlatformMax characters
instagram1,000
facebook2,000

Webhook delivery

ParameterDefaultRange
Endpoint timeout10 seconds3 – 30 seconds
Max retry attempts61 – 10
Idempotency key length1 – 128 characters
Endpoint URL lengthMax 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:

PlatformLimit
Instagram DM100 messages/second, 750 private replies/hour
Facebook Messenger300 messages/second, 750 private replies/hour

If Meta returns a rate limit error, the request is automatically queued for retry using the schedule above.

Need help?
Our team can assist with integration questions.
Contact Support