Webhooks
HMAC-signed outbound webhooks for billable status changes, patient-flow transitions, and audit events.
Updated 2026-04-24 · Edit this page on GitHub
title: "Webhooks" description: "HMAC-signed outbound webhooks for billable status changes, patient-flow transitions, and audit events." order: 3 category: "integrations" updated_at: "2026-04-24"
Rounds can push events to any HTTPS endpoint you control — clearinghouse reconciliation, a billing automation, a custom Slack notification. Every payload is HMAC-signed so you can verify it came from us.
What events are available
Configured per-subscription in /admin/webhooks. v1 supports:
billable.identified— new billable createdbillable.status_changed— submitted → paid / denied / voidedbillable.voidedpatient_flow.appointment_scheduledpatient_flow.status_changed— flow-state transitionintegration.connected/integration.disconnectedmcp.token_issued/mcp.token_revoked- more on request
Creating a subscription
/admin/webhooks→ New subscription- Endpoint URL — HTTPS only (HTTP rejected)
- Events — pick the ones you want
- Description — free text so you remember what this subscription is for
- Save — Rounds mints the signing secret + shows it once
Copy the signing secret to your infrastructure's secret store (AWS Secrets Manager, 1Password, etc.) — it's not recoverable. If lost, regenerate it in Rounds (old signatures will stop validating).
Payload shape
Every payload is:
{
"event": "billable.status_changed",
"event_id": "evt_abc123...",
"occurred_at": "2026-04-24T14:30:00Z",
"practice_id": "uuid",
"resource_type": "billable",
"resource_id": "uuid",
"data": { ... event-specific fields ... },
"meta": { "actor_id": "uuid", "actor_label": "user@practice.com" }
}
HTTP headers include:
X-Ralt-Signature: sha256=<hex>— HMAC-SHA256 of the raw body using your signing secretX-Ralt-Event: billable.status_changedX-Ralt-Event-Id: evt_abc123...X-Ralt-Delivery-Id: del_xyz789...
Verifying signatures (Node example)
import crypto from "node:crypto";
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const received = signatureHeader.replace(/^sha256=/, "");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
Always use timing-safe comparison; plain string-equality leaks timing.
Retries
If your endpoint returns non-2xx (or times out at 30s), Rounds retries with exponential backoff:
- 2nd attempt: ~1 min later
- 3rd: ~5 min
- 4th: ~15 min
- 5th: ~60 min
- 6th: ~6h
- After 6 failures, the delivery moves to the dead-letter queue
Dead-letter queue
Failed deliveries queue in the admin UI at /admin/webhooks/[subscription-id]/dead-letter. You can:
- Inspect the payload + last response body/status
- Manually retry (one-off, or flush the whole DLQ)
- Delete if the failure is expected (e.g., endpoint permanently gone)
Rate limits
- Outbound: unlimited from Rounds side; your endpoint's rate-limit gates throughput
- Inbound (if you later wire webhook replies back to Rounds): N/A, Rounds doesn't accept inbound webhooks in v1
Testing
/admin/webhooks/[subscription-id] → Send test event — fires a synthetic payload your endpoint should handle. Useful for CI smoke tests.
If you hit a problem
- Signature doesn't match: verify you're hashing the raw body bytes, not the parsed JSON; and that you're using the correct secret version
- Endpoint not receiving events: check the subscription is
status=active+ your endpoint allows Rounds' Vercel egress IPs (not typically restricted but worth verifying on security-hardened endpoints) - Events stuck in DLQ: inspect the last-response column; common causes are your endpoint returning 401 (auth issue) or 500 (bug in your handler)
- Other: support@ralthealth.com