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 automatically retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 2 minutes |
| 3rd retry | 10 minutes |
| 4th retry | 30 minutes |
| 5th retry | 2 hours |
After 6 total attempts (1 initial + 5 retries), the delivery is marked as permanently failed. All attempts are logged in the delivery log.
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
Built-in Events
| Event Type | Description |
|---|---|
intervention.delivered | Intervention was delivered to the user |
user.stage_changed | User's lifecycle stage changed |
user.risk_changed | User's risk level changed |
user.responded | User responded to an intervention |
decision.recommended | AI decision engine recommended an action |
outcome.recorded | Intervention outcome was tracked |
agent.action | Agent loop executed a tool |
External Integration Events
| Event Type | Description |
|---|---|
external_action.dispatched | Action sent to an external system (loyalty, gamification, etc.) |
external_action.completed | External action reported a successful outcome |
external_action.failed | External action failed or was rejected |
Custom Events
| Event Type | Description |
|---|---|
custom.* | Any event prefixed with custom. from external systems |
Subscribe to custom.* to receive all custom events, or subscribe to specific ones like custom.reward.redeemed. Custom events are emitted by external systems via the External Events API.
Event Subscriptions
When creating a webhook endpoint, you can filter which events it receives:
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",
"events": ["intervention.delivered", "external_action.completed", "custom.*"]
}'Omit events to receive all event types.
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).