Webhooks
Register endpoints, verify signatures, deduplicate retries, and test deliveries — including offline and from the portal.
Webhooks
The platform delivers events to your backend over HTTPS: connections authorizing, data importing, exports becoming ready. Webhooks are how you react without polling.
Register an endpoint
Create an endpoint from the developer portal
or the API. The URL must be a public https:// host on port 443 — the
dispatcher runs in the cloud and cannot reach localhost or a private IP.
const endpoint = await client.createWebhookEndpoint(
{ url: 'https://your-app.example.com/webhooks/curaeai',
eventTypes: ['records.export_ready', 'connection.revoked'] },
{ idempotencyKey: 'create-prod-endpoint' },
);
console.log(endpoint.data.signingSecret); // shown EXACTLY onceThe signingSecret is returned once, at creation. Store it in your
secret manager immediately — it's the value you verify deliveries with.
Verify every delivery
Each request carries:
| Header | Meaning |
|---|---|
CuraeAI-Signature | t=<unix_seconds>,v1=<hex_hmac> |
CuraeAI-Timestamp | signing time (unix seconds) |
CuraeAI-Event-Id | stable event id — your dedupe key (constant across retries) |
CuraeAI-Event-Type | e.g. records.export_ready |
The HMAC is HMAC-SHA256(signingSecret, "<t>.<rawBody>"). The canonical
receiver follows four rules in order:
import {
verifyWebhookSignature, WebhookSignatureError,
parsePlatformWebhookEvent, PlatformWebhookParseError,
} from '@curaeai/platform-sdk';
// 1. Read the RAW body — the signature is over the exact bytes sent.
const rawBody = await readRawBody(req);
// 2. Verify BEFORE parsing. A bad signature → 400 (terminal; no retry).
try {
await verifyWebhookSignature({
rawBody,
signatureHeader: req.headers['curaeai-signature'],
signingSecret,
});
} catch (e) {
if (e instanceof WebhookSignatureError) return reply(400);
throw e;
}
const event = parsePlatformWebhookEvent(rawBody); // typed union
// 3. Deduplicate on the stable event id (delivery is at-least-once).
if (await alreadyProcessed(event.eventId)) return reply(200);
await markProcessed(event.eventId);
// 4. Acknowledge fast (2xx), then do the work.
reply(200);verifyWebhookSignature uses Web Crypto (Node 19+ and edge runtimes), with
a 5-minute replay tolerance, and accepts any v1= segment so a
delivery sent during a signing-secret rotation still verifies.
Retries
The platform retries on 5xx and treats 4xx as terminal — so reject
bad signatures with 400 (no retry storm) and reserve 5xx for your own
transient failures. Because retries reuse the same CuraeAI-Event-Id, your
dedupe gate (a durable UNIQUE index in production) makes reprocessing a
no-op.
Event catalog
The SDK ships a discriminated union for every event — narrow on
event.eventType for exhaustive, type-safe handling.
patient.created·patient.updatedobservation.created·observation.updatedimport.completedconnection.authorized·connection.reconnected·connection.expired·connection.action_required·connection.revokedrecords.export_queued·records.export_ready·records.export_failed·records.export_expiredentry.initiated·entry.completed·entry.limited_provisioned·entry.transferred·entry.foldedconsolidation.completed·consolidation.reverseduser.graduated
Test deliveries
Three ways, from fastest to most realistic:
- Offline —
buildSignedWebhookRequest()produces a delivery byte-identical to production. POST it at your receiver in a test or from a script. No tunnel, no platform connection. - From the portal — each endpoint has a Send test event button and a per-delivery Resend, with full request/response delivery logs.
- Real — register a public endpoint and exercise the live flow.
import { buildSignedWebhookRequest } from '@curaeai/platform-sdk';
const { body, headers } = await buildSignedWebhookRequest({
signingSecret,
event: { eventType: 'records.export_ready', eventId: crypto.randomUUID(), data: {} },
});
await fetch('http://localhost:3001/webhooks/curaeai', { method: 'POST', body, headers });Next
- API conventions — errors, idempotency, versioning.
- API reference — the
/api/platform/v1/webhook-endpointscontract.
Connect
Embed the Connect ceremony so a user links their health system — with the API key kept server-side and the LIMITED→ACTIVE graduation handled for you.
API conventions
The cross-cutting contract — errors, pagination, idempotency, conditional reads, versioning, and rate limits — that every endpoint follows.