Webhook Setup#
A practical, end-to-end walkthrough for receiving Outeract events at your own endpoint: expose an endpoint, create a subscription, verify signatures, and handle retries. For the full field reference, see Outbound Webhooks.
sequenceDiagram
participant P as Platform / API
participant O as Outeract
participant Y as Your endpoint
P->>O: Event created (message.inbound, order.shipped, ...)
O->>O: Match subscription patterns
O->>Y: POST + X-Outeract-Signature
Y-->>O: 2xx (ack fast)
Y->>Y: Process async1. Expose an endpoint#
Your endpoint must accept POST requests with a JSON body. For local development, run a minimal receiver:
# receiver.py; run with: uvicorn receiver:app --port 3000
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
async def webhook(request):
body = await request.body()
print(request.headers.get("X-Outeract-Event-Type"), body[:200])
return JSONResponse({"received": True})
app = Starlette(routes=[Route("/webhooks/outeract", webhook, methods=["POST"])])Outeract needs a publicly reachable URL, so tunnel your local port with a dev tunnel of your choice (ngrok, Cloudflare Tunnel, etc.):
ngrok http 3000
# → https://abc123.ngrok.app/webhooks/outeract2. Create the subscription#
In the console, open your application’s Settings → Webhooks and create a subscription:
| Field | Value |
|---|---|
| Name | Message Handler |
| URL | https://abc123.ngrok.app/webhooks/outeract |
| Event types | message.* |
**Secret:** Leave the secret blank and Outeract generates one for you. Copy it from the subscription detail page and store it. You need it to verify signatures in the next step.
Optional settings: a signing secret, custom headers added to every delivery, max attempts (default 5), and a timeout in seconds (default 120).
Event type patterns#
Patterns are shell-style wildcards matched against the event type:
| Pattern | Matches |
|---|---|
message.inbound | Exactly that event type |
message.* | All message events |
order.* | Your custom order.shipped, order.created, … |
* | Every event (use sparingly) |
3. Verify the signature#
Every delivery is signed with HMAC-SHA256 over the raw request body, using your subscription secret:
| Header | Value |
|---|---|
X-Outeract-Signature | sha256=<hex digest>, the HMAC-SHA256 of the raw body |
X-Outeract-Event-Type | Event type, e.g. message.inbound |
X-Outeract-Event-ID | Source event UUID |
X-Outeract-Delivery-ID | Unique per delivery; use for idempotency |
X-Outeract-Timestamp | Event creation time (ISO 8601) |
X-Outeract-Dispatch-Time | When dispatch started (ms since epoch) |
X-Outeract-Event-Created | Event creation time (ISO 8601) |
**Sign the raw bytes.** Compute the HMAC over the body exactly as received. If your framework parses the JSON and you re-serialize it, key order and whitespace change and the signature will never match.
4. Respond fast, process async#
Return a 2xx status as quickly as possible; anything below 300 counts as delivered. Do the real work after acknowledging:
async def webhook(request):
raw_body = await request.body()
if not verify_signature(raw_body, request.headers["X-Outeract-Signature"], SECRET):
return JSONResponse({"error": "invalid signature"}, status_code=401)
task_queue.enqueue(process_event, raw_body) # your queue of choice
return JSONResponse({"received": True})The delivery body looks like:
{
"event_id": "0d9f1c9e-...",
"event_type": "message.inbound",
"timestamp": "2026-07-14T10:30:00+00:00",
"app_id": "8b2e4a71-...",
"data": {
"type": "message",
"message": { "text": "Hello!", "role": "user" },
"platform": "whatsapp",
"user_id": "…", "from_user_id": "…", "from_identity_id": "…"
},
"edges": [
{ "edge_type": "sent_by", "target_node_type": "identity", "target_node_id": "…" },
{ "edge_type": "in_conversation", "target_node_type": "event", "target_node_id": "…" }
]
}Delivery is at-least-once, so duplicates can occur when an acknowledgement is lost. Deduplicate on X-Outeract-Delivery-ID (or event_id if you only care about the event once, regardless of subscription).
5. Test the delivery#
In the console, open your application’s Webhooks section. Each subscription has a test action that sends a webhook.test event to your endpoint, plus per-delivery logs and timing stats (response time and end-to-end latency).
The subscription detail page shows the full delivery history: status, HTTP status, attempt count, error message, next retry time, and delivery timestamp for every attempt.
Retries and failure behaviour#
- A delivery succeeds on any HTTP status below 300.
- Non-2xx responses, timeouts (default 120 s, configurable via
timeoutSeconds, capped at 300 s), and connection errors are retried up tomaxAttemptstimes (default 5). - Retries follow an exponential schedule: 5 s, 5 m, 30 m, 2 h, 5 h after each failed attempt (±10% jitter). With the default 5 attempts a delivery is retried over roughly 2.5 hours.
backoff_delay_secondsis deprecated and ignored. - After the final attempt, the delivery is marked
failedand will not be retried. - Disabling a subscription in the console stops pending deliveries.
- Auto-disable: after 20 consecutive failures with no successful delivery in the last 24 hours, the subscription is automatically disabled and a
system.webhook_endpoint_disabledevent is emitted. The console shows the health status and the reason it was disabled; re-enable it once the endpoint is fixed (this resets the failure counters).
Delivery status values: pending (queued or awaiting retry; check nextRetryAt), success, failed.
Troubleshooting#
| Symptom | Likely cause | Fix |
|---|---|---|
| Signature never matches | Body re-serialized before hashing, or middleware rewrote it | HMAC the raw bytes exactly as received; capture the body before JSON parsing |
| Signature matches locally but not behind a proxy | Proxy/CDN modifies the body (compression, encoding) | Verify at the first hop that sees the original body |
| Deliveries time out | Handler does work before responding | Return 2xx immediately, process async; raise timeoutSeconds only as a last resort |
| Duplicate events | At-least-once delivery redelivered after a lost ack | Deduplicate on X-Outeract-Delivery-ID |
| Events arrive out of order | Retries and parallel delivery make ordering best-effort | Order by the timestamp field (event creation time), not arrival time |
| Nothing arrives | Pattern doesn’t match, subscription disabled manually, or auto-disabled after sustained failures | Check eventTypes patterns, enabled, and disabledReason; re-enable and use the console test action |
See Also#
- Outbound Webhooks - full subscription and payload reference
- Webhooks API - inbound webhooks (platforms → Outeract)
- Custom Events - emit your own events to subscribers
- Events - the event ledger model