Retivo

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/retivo

Or 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)
end

Request Format

HeaderDescription
Content-Typeapplication/json
X-Retivo-SignatureHMAC-SHA256 hex digest of the body
User-AgentRetivo-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 200 as quickly as possible
  • Process the webhook payload asynchronously (e.g., push to a job queue)
  • Log the intervention_id for 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 TypeDescription
intervention.deliveredIntervention was delivered to the user
intervention.failedDelivery failed (includes error details)
intervention.escalatedIntervention routed to human review
user.opted_outUser unsubscribed from interventions
user.lifecycle_changedUser'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).

On this page