CuraeAI Developers
Build

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 once

The 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:

HeaderMeaning
CuraeAI-Signaturet=<unix_seconds>,v1=<hex_hmac>
CuraeAI-Timestampsigning time (unix seconds)
CuraeAI-Event-Idstable event id — your dedupe key (constant across retries)
CuraeAI-Event-Typee.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.updated
  • observation.created · observation.updated
  • import.completed
  • connection.authorized · connection.reconnected · connection.expired · connection.action_required · connection.revoked
  • records.export_queued · records.export_ready · records.export_failed · records.export_expired
  • entry.initiated · entry.completed · entry.limited_provisioned · entry.transferred · entry.folded
  • consolidation.completed · consolidation.reversed
  • user.graduated

Test deliveries

Three ways, from fastest to most realistic:

  1. OfflinebuildSignedWebhookRequest() produces a delivery byte-identical to production. POST it at your receiver in a test or from a script. No tunnel, no platform connection.
  2. From the portal — each endpoint has a Send test event button and a per-delivery Resend, with full request/response delivery logs.
  3. 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

On this page