Webhooks overview¶
How webhook delivery works end-to-end. The full per-event payload schemas are in Event catalog; the signature verification recipe is in Verifying signatures. This page is the conceptual layer that ties them together.
Why webhooks (vs polling)¶
Polling burns rate-limit budget. A naive sync that polls /events/{id}/participants every minute consumes 60 of your 500 hourly budget per hour. Add /program, /activities, /locations, and you're past 200/h before any actual data arrives.
Webhooks consume zero rate-limit budget at idle. Your integration listens, reacts to the snapshot in the payload directly, and only calls the API for related data not in the snapshot (e.g. parent event metadata for an application.approved payload). Same overall data freshness, hundredth of the API load.
If you're polling, you're probably doing it wrong. Almost every "I want to know when X happens" use case has a corresponding webhook event.
Payload pattern: include data¶
Webhook payloads carry the full resource snapshot at the moment the event occurred. You can act directly on the payload without an API round-trip:
{
"type": "application.approved",
"event_id": "evt_abc123",
"data": {
"id": "app_xyz",
"participant_id": "ptp_001",
"user_id": "usr_def456",
"status": "approved",
"approved_at": "2026-05-02T09:14:00Z",
"approver_id": "usr_org_123",
"custom_form_fields": { /* ... */ }
}
}
Saves a round-trip per event, reduces rate-limit pressure, and makes your handler self-contained.
If you need related data not in the payload (e.g. you received application.approved and want the full event metadata too), call the Read API with your installation token.
The one exception is data.deletion_required — its payload is minimal because the only action is "delete," not "process."
Subscribing¶
You don't manage subscription objects at runtime. Your integration manifest declares:
webhook_url— single HTTPS endpoint where every delivery POSTsdeclared_webhook_event_types— the array of event types you want to receive
When a token is issued (organizer connects your integration to an event), deliveries for the declared event types flow to your URL automatically. No per-installation subscription step required.
At registration you receive:
- An HMAC signing secret — generated by Revento, shown to you once. Afterwards we store only the hash. If you lose it, rotate.
To change which event types you receive, edit your integration manifest. To pause deliveries entirely, clear declared_webhook_event_types in the manifest; to resume, re-add.
Signed delivery¶
Every delivery has these headers:
Content-Type: application/json
X-Revento-Event: application.approved
X-Revento-Delivery-Id: 3f9b1e2a-7e8f-4d5c-bc0a-1f2e3d4c5b6a
X-Revento-Timestamp: 1747000123
X-Revento-Signature: sha256=8e1c4b...
The X-Revento-Signature is HMAC-SHA256 of {timestamp}.{raw_body} using your integration's webhook signing secret. Verify this on every delivery before trusting the body. See Verifying signatures.
The X-Revento-Delivery-Id is unique per delivery — but retries reuse it. Use it as a dedup key.
The 5-minute replay window is your responsibility: reject deliveries where |now - timestamp| > 300 seconds.
The 10-second timeout¶
Each delivery attempt has 10 seconds to receive a 2xx response. If your handler takes longer, Revento closes the connection and counts the delivery as failed for retry purposes.
Implication: respond quickly, queue real work async on your side.
@app.route("/webhooks/revento", methods=["POST"])
def webhook():
verify_signature() # cheap
payload = request.json
# Queue real work — don't block the response
queue.enqueue("process_webhook", payload, request.headers["X-Revento-Delivery-Id"])
return "ok", 200 # respond fast
Retry curve¶
If you respond non-2xx (or time out at 10s), we retry with this curve:
| Attempt | Delay after previous failure |
|---|---|
| 1 (initial) | — |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
| 7 | 24 hours |
| Dead-letter | After 7th failure |
After 6 retries (7 total attempts: initial + 6 retries) failing, the delivery moves to dead-letter. You can replay individual dead-letter deliveries from the developer console.
The 30-second first retry can drift up to ~90 seconds in practice, depending on dispatch scheduling. Don't assume exact timing.
When we don't retry¶
We retry on:
- 5xx responses
- 408 Request Timeout
- 429 Too Many Requests (rare from your end, but valid)
- Network errors / connection failures
- Our 10-second per-attempt timeout
We do not retry on:
- 4xx other than 408/429 — interpreted as "you're permanently unable to process this payload"
- Successful 2xx (obviously)
A 400/401/403/404 from your endpoint moves the delivery directly to dead-letter. Get your handler right or the dead-letter queue fills up.
No static outbound IP¶
Webhook deliveries originate from Revento Cloud's egress without a stable public IP. Don't try to allowlist us by IP — use signature verification (the only supported method).
If your network policy strictly requires IP-based allowlisting, you'll need a mutually-authenticated reverse proxy or a similar pattern. Revento doesn't promise a stable origin IP.
Dead-letter inspection and replay¶
The developer console has a "Failed deliveries" tab listing each dead-lettered delivery:
- Timestamp
- Event type
- Last status code from your endpoint
- Number of attempts
- "Replay" button to re-dispatch this single delivery
Replay sends a fresh delivery with a new X-Revento-Delivery-Id. Your handler should treat it as a new attempt, not as a duplicate of the original (it's a different delivery from the system's perspective, even if the payload is identical).
Secret rotation¶
To rotate your integration's webhook signing secret:
See API → endpoints → developer endpoints for the full request/response shape.
This:
- Generates a new secret, returned once in the response.
- Keeps the old secret valid for 24 hours alongside the new one.
- During the overlap, deliveries are signed with both secrets — Revento sends two
X-Revento-Signatureheaders, your verifier should accept either. - After 24 hours, the old secret is invalidated.
The 24-hour overlap lets you roll the new secret to all your servers without a downtime window.
Rotation is the recommended response to a suspected secret leak — it's also a fine occasional hygiene practice.
Changing which events you receive¶
To add or remove event types: edit your integration manifest's declared_webhook_event_types. Future deliveries reflect the updated set immediately. Past events are not replayed unless you explicitly request a replay (replays are bounded — typically 7 days of history).
Pause your manifest's webhook handling entirely by removing all entries from declared_webhook_event_types. While none are declared, no deliveries fire. Re-add later to resume.
What happens if we lose your endpoint¶
If your endpoint is unreachable for an extended period, deliveries pile up in dead-letter. There's no automatic re-enable — once you've fixed your endpoint, you replay individual dead-letter deliveries from the developer console, or accept the gap and reconcile via API fetch.
For long outages (>24h), reconciling via API is more practical than replaying every dead-lettered delivery.