Webhooks
Subscriptions and outbound webhooks are stable: bearer-API-key callers can register subscriptions under /api/subscriptions and /api/intake/{formId}/subscriptions, and Blue Banana signs and delivers criteria.matched, template.fired, and intake.submission.created events to the registered URL.
When a subscription with an http destination fires, Blue Banana POSTs a signed JSON payload to your URL. Two event types in v1: criteria.matched (immediate, fired from FHIR writes) and template.fired (time-anchored, fired by the cron sweeper).
The full payload schema lives in the API reference under Webhooks — this page covers signing, retries, and how to register a subscription.
1 · Register a subscription
POST /api/subscriptions with an http destination. The server generates a 64-character hex secret and returns it in secret_view on the create response exactly once. Store it — subsequent reads of the subscription will not expose it again.
curl -X POST https://bluebananaehr.com/api/subscriptions \
-H "Authorization: Bearer bb_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Forward new observations",
"enabled": true,
"trigger": { "kind": "criteria", "resourceType": "Observation" },
"destination": {
"kind": "http",
"url": "https://example.com/keragon"
}
}'2 · Delivery headers
Content-Type: application/jsonX-Keragon-Signature: t=<unix>,v1=<hex>— Stripe-shape HMAC SHA-256.X-Keragon-Delivery: del_…— stable id you can use for idempotency.- Any extra static headers you set on the subscription
destination.headers.
3 · Verify the signature
Recompute the signature over ${t}.${rawBody} with the subscription's secret and constant-time compare. Reject if the timestamp drifts more than 5 minutes from your clock — defeats replay.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody: string, header: string, secret: string): boolean {
const m = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header.trim());
if (!m) return false;
const t = Number(m[1]);
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(m[2], 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}4 · Retries
Blue Banana treats any 2xx as success. Non-2xx and network errors are retried with backoff 0s → 30s → 5m → 30m → 6h. After 5 attempts the delivery is marked failed and not retried again.
Use X-Keragon-Delivery to dedupe — the same id is sent on every retry of a single delivery.
5 · Event types
criteria.matched
Fired immediately when a FHIR POST or PUT hits a resource that matches a criteria-trigger subscription. Match against either fieldFilters (deprecated, top-level string equality) or fhirpath (preferred — an array of boolean FHIRPath expressions, AND-combined, parsed with the HL7-maintained engine). The body carries the just-written resource.
template.fired
Fired by the cron sweeper for time-anchored triggers:appointment_reminder, appointment_no_show, daily_at, and cron (free-form 5-field UTC expression). For Appointment-anchored templates the body carries the Appointment; for daily_at and cron, resource is null.