Webhooks
Receive outbound intervention delivery via HTTP callbacks with HMAC signing
Webhooks let Retivo deliver intervention data to your backend when the webhook channel is used. Every webhook request is signed with HMAC-SHA256 so you can verify authenticity.
Setup
1. Register a Webhook Endpoint
In the dashboard under Settings > Integrations, add a webhook URL:
https://your-app.com/webhooks/retivoOr via the API:
curl -X POST https://retivo.ai/api/webhooks/endpoints \
-H "Authorization: Bearer rt_live_..." \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-app.com/webhooks/retivo" }'Retivo generates an HMAC secret for each endpoint. Store this secret securely — you'll need it to verify signatures.
2. Receive Webhook Events
When an intervention is delivered via the webhook channel, Retivo sends a POST request:
{
"intervention_id": "intv_abc123",
"user_id": "user-123",
"playbook_id": "pb_xyz",
"channel": "webhook",
"message_body": "Hi Jane! Have you tried our new sprint planner?",
"reasoning": "User is in onboarding with low activation score",
"confidence": 0.85,
"metadata": {
"lifecycle_stage": "onboarding",
"activation_score": 0.23,
"risk_level": "medium"
}
}3. Verify the Signature
Every request includes an X-Retivo-Signature header containing the HMAC-SHA256 hex digest of the raw request body.
Node.js example:
import crypto from 'crypto'
function verifyWebhook(body: string, signature: string, secret: string): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}
// In your Express/Hono/Next.js handler:
app.post('/webhooks/retivo', (req, res) => {
const rawBody = req.rawBody // must be the raw string, not parsed JSON
const signature = req.headers['x-retivo-signature']
if (!verifyWebhook(rawBody, signature, process.env.RETIVO_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' })
}
const payload = JSON.parse(rawBody)
// Process the intervention...
res.status(200).json({ received: true })
})Python example:
import hmac
import hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Go example:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifyWebhook(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}Ruby example:
require 'openssl'
def verify_webhook(body, signature, secret)
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, body)
Rack::Utils.secure_compare(signature, expected)
endRequest Format
| Header | Description |
|---|---|
Content-Type | application/json |
X-Retivo-Signature | HMAC-SHA256 hex digest of the body |
User-Agent | Retivo-Webhook/1.0 |
Retry Behavior
If your endpoint returns a non-2xx status code or the request times out (10 seconds), Retivo will not automatically retry. Failed deliveries are logged in the audit trail with the HTTP status code.
To ensure reliable delivery:
- Return
200as quickly as possible - Process the webhook payload asynchronously (e.g., push to a job queue)
- Log the
intervention_idfor deduplication
Testing
Use a test API key (rt_test_...) to trigger webhook deliveries without affecting production data. Combine with the dry-run API to preview what would be sent.
You can also use services like webhook.site or ngrok to inspect webhook payloads during development.
Webhook Event Types
| Event Type | Description |
|---|---|
intervention.delivered | Intervention was delivered to the user |
intervention.failed | Delivery failed (includes error details) |
intervention.escalated | Intervention routed to human review |
user.opted_out | User unsubscribed from interventions |
user.lifecycle_changed | User's lifecycle stage changed |
Example Payloads
intervention.delivered
{
"event": "intervention.delivered",
"intervention_id": "intv_abc123",
"user_id": "user-123",
"playbook_id": "pb_xyz",
"channel": "webhook",
"delivered_at": "2026-04-05T14:22:00Z"
}intervention.failed
{
"event": "intervention.failed",
"intervention_id": "intv_abc456",
"user_id": "user-456",
"channel": "email",
"error": {
"code": "delivery_rejected",
"message": "Upstream provider returned 550 mailbox not found"
},
"failed_at": "2026-04-05T14:23:00Z"
}intervention.escalated
{
"event": "intervention.escalated",
"intervention_id": "intv_abc789",
"user_id": "user-789",
"playbook_id": "pb_xyz",
"reason": "Confidence below threshold (0.42)",
"escalated_at": "2026-04-05T14:24:00Z"
}user.opted_out
{
"event": "user.opted_out",
"user_id": "user-321",
"opted_out_at": "2026-04-05T14:25:00Z",
"channel": "all"
}user.lifecycle_changed
{
"event": "user.lifecycle_changed",
"user_id": "user-654",
"previous_stage": "onboarding",
"new_stage": "active",
"changed_at": "2026-04-05T14:26:00Z"
}Troubleshooting
Signature Mismatch
The most common cause is verifying against a parsed/re-serialized JSON body instead of the raw request bytes. HMAC verification must use the exact bytes that Retivo sent. In Express, use req.rawBody; in other frameworks, capture the body before any JSON parsing middleware runs.
Timeout Handling
Retivo waits 10 seconds for a response. If your processing takes longer, return 200 immediately and handle the work asynchronously via a job queue (e.g., BullMQ, Celery, Sidekiq). This prevents timeouts and ensures Retivo records the delivery as successful.
Idempotency
Network issues can occasionally cause duplicate deliveries. Use the intervention_id field to deduplicate events. Before processing, check whether you have already handled that ID. A simple approach is to store processed IDs in a set (in-memory, a cache, or a database unique constraint).