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 created
  • billable.status_changed — submitted → paid / denied / voided
  • billable.voided
  • patient_flow.appointment_scheduled
  • patient_flow.status_changed — flow-state transition
  • integration.connected / integration.disconnected
  • mcp.token_issued / mcp.token_revoked
  • more on request

Creating a subscription

  1. /admin/webhooksNew subscription
  2. Endpoint URL — HTTPS only (HTTP rejected)
  3. Events — pick the ones you want
  4. Description — free text so you remember what this subscription is for
  5. 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 secret
  • X-Ralt-Event: billable.status_changed
  • X-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