Outbound Webhooks#
Subscribe to Outeract events and receive them at your endpoint in real-time.
Overview#
Outbound webhooks allow you to:
- Receive messages as they arrive
- Track message delivery status
- React to custom events
- Build real-time integrations
Creating a Subscription#
GraphQL#
mutation {
createWebhookSubscription(
name: "Message Handler"
targetUrl: "https://myapp.com/webhooks/outeract"
eventPatterns: ["message.inbound", "message.outbound"]
secret: "my-webhook-secret-32chars-minimum"
) {
id
name
targetUrl
eventPatterns
status
}
}REST#
curl -X POST https://api.outeract.com/v1/webhook-subscriptions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Outeract-App-ID: app_abc123" \
-H "Content-Type: application/json" \
-d '{
"name": "Message Handler",
"target_url": "https://myapp.com/webhooks/outeract",
"event_patterns": ["message.inbound", "message.outbound"],
"secret": "my-webhook-secret-32chars-minimum"
}'Event Patterns#
Event patterns determine which events trigger your webhook.
Exact Match#
["message.inbound", "message.outbound"]Only matches these exact event types.
Wildcard Patterns#
["message.*"]Matches any event starting with message.:
message.inboundmessage.outboundmessage.deliveredmessage.read
All Events#
["*"]Matches all events (use with caution).
Common Patterns#
| Pattern | Matches |
|---|---|
message.* | All message events |
message.inbound | Incoming messages only |
message.outbound | Outgoing messages only |
message.status | Delivery status updates |
custom.* | All custom events |
user.* | User-related events |
link_code.* | Link code events |
Webhook Payload#
Structure#
{
"id": "whd_abc123",
"subscription_id": "ws_xyz789",
"event_id": "evt_123456",
"event_type": "message.inbound",
"app_id": "app_abc123",
"payload": {
"message": {
"text": "Hello!",
"role": "user"
},
"platform": "whatsapp",
"external_message_id": "wamid.xyz"
},
"edges": {
"sent_by": {
"platform_user_id": "pu_xyz789",
"external_id": "+14155551234",
"name": "John Doe"
},
"sent_to": {
"platform_user_id": "pu_system",
"external_id": "+14155550000",
"is_system_user": true
}
},
"created_at": "2024-01-15T10:30:00Z",
"timestamp": "2024-01-15T10:30:01Z"
}Fields#
| Field | Type | Description |
|---|---|---|
id | string | Unique delivery ID |
subscription_id | string | Your subscription ID |
event_id | string | Source event ID |
event_type | string | Event type (e.g., message.inbound) |
app_id | string | Your application ID |
payload | object | Event-specific data |
edges | object | Related entities (sender, recipient, files) |
created_at | datetime | When event was created |
timestamp | datetime | Delivery timestamp |
Verifying Webhooks#
Every webhook includes a signature for verification.
Headers#
X-Outeract-Signature: sha256=abc123...
X-Outeract-Timestamp: 1705320600
X-Outeract-Delivery-ID: whd_abc123Verification (Python)#
import hmac
import hashlib
import time
def verify_webhook(
body: bytes,
signature: str,
timestamp: str,
secret: str,
tolerance: int = 300 # 5 minutes
) -> bool:
# Check timestamp to prevent replay attacks
current_time = int(time.time())
webhook_time = int(timestamp)
if abs(current_time - webhook_time) > tolerance:
return False
# Construct signed payload
signed_payload = f"{timestamp}.{body.decode()}"
# Calculate expected signature
expected = "sha256=" + hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected, signature)Verification (Node.js)#
const crypto = require('crypto');
function verifyWebhook(body, signature, timestamp, secret, tolerance = 300) {
// Check timestamp
const currentTime = Math.floor(Date.now() / 1000);
const webhookTime = parseInt(timestamp);
if (Math.abs(currentTime - webhookTime) > tolerance) {
return false;
}
// Calculate expected signature
const signedPayload = `${timestamp}.${body}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}Responding to Webhooks#
Success Response#
Return HTTP 200-299 within 30 seconds:
{
"received": true
}Or simply:
HTTP/1.1 200 OKError Response#
If processing fails, return 4xx or 5xx:
HTTP/1.1 500 Internal Server Error
{
"error": "Processing failed"
}The webhook will be retried according to the retry policy.
Retry Policy#
Failed deliveries are retried with exponential backoff:
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 30 minutes | 36 minutes |
| 5 | 2 hours | ~2.5 hours |
After 5 failures, the delivery is marked as failed.
Retry Headers#
On retries, additional headers are included:
X-Outeract-Retry-Count: 2
X-Outeract-Original-Timestamp: 1705320600Managing Subscriptions#
List Subscriptions#
query {
webhookSubscriptions {
id
name
targetUrl
eventPatterns
status
createdAt
}
}Update Subscription#
mutation {
updateWebhookSubscription(
id: "ws_abc123"
name: "Updated Name"
eventPatterns: ["message.*", "custom.*"]
) {
id
name
eventPatterns
}
}Pause Subscription#
mutation {
updateWebhookSubscription(
id: "ws_abc123"
status: PAUSED
) {
id
status
}
}Delete Subscription#
mutation {
deleteWebhookSubscription(id: "ws_abc123")
}Delivery Logs#
View webhook delivery history:
query {
webhookDeliveryLogs(
webhookSubscriptionId: "ws_abc123"
first: 20
) {
edges {
node {
id
eventId
eventType
status
statusCode
attemptCount
duration
errorMessage
createdAt
}
}
}
}Status Values#
| Status | Description |
|---|---|
pending | Queued for delivery |
delivered | Successfully delivered |
failed | All retries exhausted |
retrying | Failed, will retry |
Testing Webhooks#
Test Endpoint#
Send a test event to your webhook:
mutation {
testWebhookSubscription(id: "ws_abc123") {
success
statusCode
responseBody
duration
}
}Webhook Debugging Tools#
- webhook.site - Inspect payloads online
- ngrok - Expose local server
- RequestBin - Collect and inspect requests
Local Development#
# Start ngrok
ngrok http 3000
# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok.io/webhooks/outeractBest Practices#
1. Return Fast#
Process webhooks asynchronously:
from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
@app.post("/webhooks/outeract")
async def webhook(request: Request, background_tasks: BackgroundTasks):
body = await request.body()
# Verify signature
if not verify_webhook(body, request.headers):
return JSONResponse(status_code=401)
# Queue for async processing
background_tasks.add_task(process_event, json.loads(body))
return {"received": True}2. Handle Duplicates#
Use event_id for idempotency:
processed_events = set() # Use Redis/DB in production
async def process_event(data):
event_id = data["event_id"]
if event_id in processed_events:
return # Already processed
processed_events.add(event_id)
# Process event...3. Secure Your Endpoint#
- Always verify signatures
- Use HTTPS
- Validate timestamp to prevent replay attacks
- Keep your secret secure
4. Monitor and Alert#
- Track webhook delivery success rate
- Alert on high failure rates
- Monitor response times
5. Use Meaningful Patterns#
Subscribe only to events you need:
// Good - specific patterns
["message.inbound", "message.status"]
// Avoid - too broad
["*"]Event Types Reference#
Message Events#
| Event Type | Description |
|---|---|
message.inbound | Incoming message received |
message.outbound | Outgoing message sent |
message.delivered | Message delivered |
message.read | Message read |
message.failed | Message delivery failed |
message.reaction | Message reaction |
Link Code Events#
| Event Type | Description |
|---|---|
link_code.generated | Link code created |
link_code.activation | Link code used |
User Events#
| Event Type | Description |
|---|---|
user.merged | Users merged |
Custom Events#
Your custom event types:
custom.order.created
custom.payment.completed
custom.support.ticket.opened