Webhooks#
Outeract uses webhooks to deliver real-time events from messaging platforms to your application.
Types of Webhooks#
Inbound Webhooks (Platform → Outeract)#
Receive messages and events from platforms (WhatsApp, Telegram, etc.). These are automatically configured when you create platform connections.
Outbound Webhooks (Outeract → Your App)#
Subscribe to events and receive them at your endpoint. Configure these to process messages in real-time.
Inbound Webhook Architecture#
Shared Webhooks#
Meta platforms (WhatsApp, Instagram, Facebook) use shared webhooks:
Platform ──► Shared Webhook URL ──► Route by Account ID ──► Connection- One URL for all connections of that platform
- Routing based on business account/page ID
- Configure once in Meta App Dashboard
Dedicated Webhooks#
Other platforms use dedicated webhooks:
Platform ──► Unique Webhook URL ──► Specific Connection- Each connection has its own URL
- URL includes connection ID and secret
- Configure in platform’s settings
Webhook URL Format#
All webhooks (both shared and dedicated) use the same URL format:
https://api.outeract.com/webhooks/{webhook_id}/{webhook_secret}Example: https://api.outeract.com/webhooks/550e8400-e29b-41d4-a716-446655440000/a1b2c3d4e5f6
- webhook_id: UUID identifying the webhook
- webhook_secret: Secret token for authentication
The difference between shared and dedicated webhooks is in how incoming messages are routed internally, not in the URL format.
Webhook Verification#
Meta Platforms (WhatsApp, Instagram, Facebook)#
Meta sends a verification request when you set up webhooks:
GET /webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=CHALLENGE_STRINGOuteract automatically responds with the challenge if the verify token matches.
Telegram#
Telegram sets webhooks via API call. Outeract provides the URL; you configure it:
curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \
-d '{"url": "YOUR_WEBHOOK_URL"}'Slack#
Slack sends a URL verification challenge:
{
"type": "url_verification",
"challenge": "challenge_string"
}Outeract responds with the challenge to verify ownership.
Signature Verification#
All platforms use cryptographic signatures to verify webhook authenticity.
Meta Platforms (HMAC-SHA256)#
X-Hub-Signature-256: sha256=abc123...Verification:
import hmac
import hashlib
def verify_meta_signature(body: bytes, signature_header: str, app_secret: str) -> bool:
expected = "sha256=" + hmac.new(
app_secret.encode(),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)Slack (HMAC-SHA256 with timestamp)#
X-Slack-Signature: v0=abc123...
X-Slack-Request-Timestamp: 1234567890Verification:
def verify_slack_signature(body: bytes, timestamp: str, signature: str, signing_secret: str) -> bool:
sig_basestring = f"v0:{timestamp}:{body.decode()}"
expected = "v0=" + hmac.new(
signing_secret.encode(),
sig_basestring.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Twilio (HMAC-SHA1)#
X-Twilio-Signature: abc123...Discord (Ed25519)#
X-Signature-Ed25519: abc123...
X-Signature-Timestamp: 1234567890Webhook Payloads#
Message Received#
When a user sends a message:
{
"type": "message.inbound",
"data": {
"event_id": "evt_abc123",
"platform": "whatsapp",
"external_message_id": "wamid.xyz",
"message": {
"text": "Hello!",
"type": "text"
},
"sender": {
"platform_user_id": "pu_xyz789",
"external_id": "+14155551234",
"name": "John Doe"
},
"timestamp": "2024-01-15T10:30:00Z"
}
}Media Message#
{
"type": "message.inbound",
"data": {
"event_id": "evt_abc123",
"platform": "whatsapp",
"message": {
"type": "image",
"media": {
"id": "file_xyz789",
"mime_type": "image/jpeg",
"url": "https://storage.outeract.com/files/..."
},
"caption": "Check this out!"
},
"sender": {
"external_id": "+14155551234"
}
}
}Message Status Update#
{
"type": "message.status",
"data": {
"event_id": "evt_status123",
"origin_event_id": "evt_abc123",
"platform": "whatsapp",
"external_message_id": "wamid.xyz",
"status": "delivered",
"timestamp": "2024-01-15T10:30:05Z"
}
}Status values:
sent- Message sent to platformdelivered- Delivered to recipient’s deviceread- Recipient read the messagefailed- Delivery failed
Reaction#
{
"type": "message.reaction",
"data": {
"event_id": "evt_react123",
"platform": "whatsapp",
"reaction": {
"emoji": "👍",
"message_id": "wamid.xyz"
},
"sender": {
"external_id": "+14155551234"
}
}
}Response Requirements#
Your webhook endpoint must:
- Return 200-299 status within 30 seconds
- Return quickly - process asynchronously if needed
- Be idempotent - handle duplicate deliveries
Example response:
{
"received": true
}Or simply:
HTTP/1.1 200 OKRetry Policy#
If your endpoint fails, Outeract retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the webhook is marked as failed.
Debugging Webhooks#
Webhook Logs#
View webhook delivery logs in the console:
query {
webhookDeliveryLogs(
webhookSubscriptionId: "ws_abc123"
first: 20
) {
edges {
node {
id
status
statusCode
requestBody
responseBody
duration
createdAt
}
}
}
}Testing Locally#
Use ngrok or webhook.site for local development:
ngrok http 3000Then use the ngrok URL as your webhook endpoint.
Best Practices#
1. Verify Signatures Always#
Never process webhooks without verifying the signature.
2. Return Fast#
Return 200 immediately, process asynchronously:
@app.post("/webhook")
async def webhook(request: Request, background_tasks: BackgroundTasks):
# Verify signature first
verify_signature(request)
# Queue for async processing
background_tasks.add_task(process_webhook, await request.json())
return {"received": True}3. Handle Duplicates#
Webhooks may be delivered multiple times. Use event_id for deduplication:
processed_events = set()
def process_webhook(data):
event_id = data["data"]["event_id"]
if event_id in processed_events:
return # Already processed
processed_events.add(event_id)
# Process event...4. Log Everything#
Log webhook payloads for debugging:
import logging
logger = logging.getLogger(__name__)
@app.post("/webhook")
async def webhook(request: Request):
body = await request.body()
logger.info(f"Webhook received: {body.decode()}")
# Process...5. Monitor Health#
Set up alerts for webhook failures and high latency.
Security Considerations#
- Always use HTTPS - Webhooks must be served over TLS
- Verify signatures - Reject requests with invalid signatures
- Validate timestamps - Reject old requests (prevent replay attacks)
- Use secrets - Keep webhook secrets secure
- Rotate secrets - Periodically rotate webhook secrets