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 async

1. 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/outeract

2. Create the subscription#

In the console, open your application’s Settings → Webhooks and create a subscription:

FieldValue
NameMessage Handler
URLhttps://abc123.ngrok.app/webhooks/outeract
Event typesmessage.*
**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:

PatternMatches
message.inboundExactly 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:

HeaderValue
X-Outeract-Signaturesha256=<hex digest>, the HMAC-SHA256 of the raw body
X-Outeract-Event-TypeEvent type, e.g. message.inbound
X-Outeract-Event-IDSource event UUID
X-Outeract-Delivery-IDUnique per delivery; use for idempotency
X-Outeract-TimestampEvent creation time (ISO 8601)
X-Outeract-Dispatch-TimeWhen dispatch started (ms since epoch)
X-Outeract-Event-CreatedEvent creation time (ISO 8601)
```python import hmac import hashlib def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature_header) # In your handler: # 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) ```
```javascript const crypto = require('crypto'); function verifySignature(rawBody, signatureHeader, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) // Buffer or string of the raw body .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signatureHeader) ); } // Express: capture the raw body before JSON parsing // app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } })); ```
**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 to maxAttempts times (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_seconds is deprecated and ignored.
  • After the final attempt, the delivery is marked failed and 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_disabled event 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#

SymptomLikely causeFix
Signature never matchesBody re-serialized before hashing, or middleware rewrote itHMAC the raw bytes exactly as received; capture the body before JSON parsing
Signature matches locally but not behind a proxyProxy/CDN modifies the body (compression, encoding)Verify at the first hop that sees the original body
Deliveries time outHandler does work before respondingReturn 2xx immediately, process async; raise timeoutSeconds only as a last resort
Duplicate eventsAt-least-once delivery redelivered after a lost ackDeduplicate on X-Outeract-Delivery-ID
Events arrive out of orderRetries and parallel delivery make ordering best-effortOrder by the timestamp field (event creation time), not arrival time
Nothing arrivesPattern doesn’t match, subscription disabled manually, or auto-disabled after sustained failuresCheck eventTypes patterns, enabled, and disabledReason; re-enable and use the console test action

See Also#