Skip to main content
BeaconLife Sciences

Webhooks

Instead of polling, your system can receive signed webhook deliveries when things happen in the registry — enrollments, signed consents, submitted patient-reported outcomes, queries, exports, and study locks.

Registering an endpoint

Register an HTTPS endpoint via the webhooks resource (or by an administrator in the admin interface). Each registration gets a unique signing secret — treat it like a password and store it in your secrets manager. Deliveries are POSTs with a JSON body describing the event.

Events

Webhook event types
EventFires when
participant.createdA participant was enrolled in a study.
consent.signedA participant completed consent; the signed document is available via the consent endpoints.
epro.response.submittedA participant submitted an ePRO instrument response.
query.openedA data query was raised against a record, awaiting site response.
export.completedAn export job finished; the download URL is available for a limited time.
study.lockedA study was locked against further data changes (for example at analysis time).

Event bodies are small and reference-based: they carry the event type, event ID, study and resource identifiers, and a timestamp — not full payloads. Fetch the resource itself with your API token when you need the details.

Verifying signatures

Every delivery is signed with HMAC-SHA256 over the delivery timestamp and the exact request body, so you can confirm it came from BeaconCR and that the bytes were not altered in transit. Two headers carry what you need:

  • Beacon-Signature: t=<unix-seconds>,v1=<hex digest> where v1 = HMAC_SHA256(secret, "<t>.<rawBody>")
  • Beacon-Timestamp: the same <unix-seconds>, as a plain header for convenience

Verify before you trust: recompute the digest over the raw request body (not a re-serialized parse) and reject deliveries whose timestamp is older than 5 minutes so a captured request cannot be replayed.

import crypto from "node:crypto";

/**
 * Verify a BeaconCR webhook delivery.
 * rawBody must be the raw request body — not a re-serialized parsed object.
 * The signature header carries the delivery timestamp and the HMAC-SHA256
 * digest (hex) of "<timestamp>.<raw body>".
 */
export function verifyWebhookDelivery(
  rawBody,
  signatureHeader,
  signingSecret,
  maxAgeSeconds = 300
) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((pair) => pair.trim().split("="))
  );

  if (!parts.t || !parts.v1) return false;

  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");

  const provided = Buffer.from(parts.v1, "hex");
  const matches =
    provided.length === expected.length / 2 &&
    crypto.timingSafeEqual(provided, Buffer.from(expected, "hex"));

  // Reject stale deliveries so a captured request cannot be replayed later.
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= maxAgeSeconds;

  return matches && fresh;
}

Retry and backoff policy

If your endpoint does not return a 2xx response, the delivery is retried with exponential backoff:

Retry schedule for failed webhook deliveries
AttemptDelay after previous attempt
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry12 hours

That is 5 retries after the initial attempt (6 attempts total). After the final attempt fails, the delivery is marked failed and no further attempts are made. Endpoints failing continuously are marked failing; a subscription that keeps failing may be disabled and must be re-enabled via PATCH /webhooks/{id}. Design handlers to be idempotent: the same event may be delivered more than once (for example, if a response is lost after your handler has committed work) — deduplicate on the event ID.

Delivery log

Every delivery attempt — successful or not — is recorded and readable two ways: via the API at GET /api/v1/webhooks/{webhookId}/deliveries (attempt number, response status, error, latency, next retry) and in the admin interface (/admin/webhooks): event type, target URL, response status, attempt number, and timestamps. A missed or failing integration can be diagnosed without contacting support. Deliveries are not replayed automatically once the retry budget is exhausted; re-point a registration or trigger a manual retry to catch events the endpoint missed.

Endpoint requirements (SSRF policy)

In production, delivery targets must be public HTTPS endpoints: loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16, fe80::/10), RFC 1918 private ranges (10/8, 172.16/12, 192.168/16) and plain-HTTP URLs are refused. Non-production environments (development, staging test runs) allow local HTTP receivers so integrations can be exercised against a loopback test server; operators can override the production refusal with BEACON_WEBHOOK_ALLOW_PRIVATE=1 when an internal receiver is the intended target.

Best practices

  • Respond 2xx quickly — do heavy work asynchronously after acknowledging the delivery.
  • Verify the signature before processing the payload, and check the timestamp before acting.
  • Do not reject deliveries on unknown event types — new event types may be added; ignore what you do not handle.
  • Keep the endpoint reachable and monitor the delivery log for gaps.