Skip to main content

Webhooks

Webhooks allow you to receive real-time notifications when events occur in your Eirly account. Instead of polling the API, webhooks push data to your application as events happen.

Available Events

Webhooks are triggered for the following events:

  • referral.created - Fired when a new referral is created
  • referral.updated - Fired when a referral is updated
  • referral.billing_failed - Fired when the billing failed for the REST api request
  • referral.pending_verification - Fired when the profile requires completion before referral can be issued
  • referral.complete - All results are completed for this referral
  • referral.insufficient_data - Unable to proceed with completion on this referral
  • referral.refunded - If the referral was refunded
  • referral.failed_test - If a test on the referral failed and resulted in a new referral being required
  • referral.partial_results - We have partial results for this referral, can be fired multiple times.
  • referral.transferred - If the original referral was transferred to another person
  • referral.issued - The referral has been issued to the customer
  • referral.scheduled_reissue - The referral is about to be reissued to another customer
  • result.created - Fired when a result is created
  • result.updated - Fired when a result is updated
  • result.complete - All results completed
  • result.in_review - The results are in review with the org, and will be released upon review
  • result.partial_results - We have partial results for this result.

Setup

Configure webhooks in your organisation settings page: Organisation => Settings => Api Keys

Webhook configuration

To configure a webhook:

  1. Navigate to Organisation Settings > API Keys
  2. Enter your webhook endpoint URL
  3. Select the events you want to receive
  4. Save your configuration

You can configure multiple webhooks with different URLs for different events, or use a single URL for all events.

Receiving Webhooks

Endpoint Requirements

Your webhook endpoint must:

  • Respond within 3 seconds - Longer responses will be considered failed
  • Return a 2xx HTTP status code - Any other status code triggers a retry
  • Use HTTPS (recommended) - Ensures secure transmission of data
  • Be publicly accessible - Eirly servers must be able to reach your endpoint

Event Ordering

Events are delivered in order per entity. For example:

  • You will always receive referral.created before referral.updated for the same referral
  • You will always receive result.created before result.updated for the same result
Sequential Delivery

If a webhook fails to deliver, all subsequent webhooks for that specific entityUuid will be queued until the failed webhook is successfully delivered or exhausts all retry attempts.

Retry Strategy

If webhook delivery fails, Eirly will retry according to this backoff schedule:

AttemptDelayFormula
1ImmediateInitial attempt
24 minutes2² minutes
39 minutes3² minutes
416 minutes4² minutes
525 minutes5² minutes

Formula: attemptCount² minutes

After 5 failed attempts, the webhook will no longer be retried and you'll need to poll the API to retrieve the missed data.

Security

Webhook Signatures

Every webhook request includes an x-bgt-hmac-sha256 header containing an HMAC signature of the payload. This signature allows you to verify that the webhook was sent by Eirly and hasn't been tampered with.

Best Practice

Always verify webhook signatures in production to prevent unauthorized requests to your endpoint.

Verifying Signatures

Use your API secret key to verify the signature:

Node.js Example:

const crypto = require("crypto");

function verifyWebhookSignature(payload, secretKey, signature) {
// Calculate expected signature
const expectedSignature = crypto
.createHmac("sha256", secretKey)
.update(payload)
.digest("hex");

// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, "hex"),
Buffer.from(signature, "hex")
);
}

// Usage in an Express endpoint
app.post(
"/webhooks/bgt",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-bgt-hmac-sha256"];
const payload = req.body.toString();

if (
!verifyWebhookSignature(payload, process.env.BGT_SECRET_KEY, signature)
) {
return res.status(401).send("Invalid signature");
}

// Process webhook
const event = JSON.parse(payload);
// ... handle event ...

res.status(200).send("OK");
}
);

Python Example:

import hmac
import hashlib

def verify_webhook_signature(payload: bytes, secret_key: str, signature: str) -> bool:
expected_signature = hmac.new(
secret_key.encode(),
payload,
hashlib.sha256
).hexdigest()

return hmac.compare_digest(expected_signature, signature)

Webhook Headers

Each webhook request includes the following headers:

HeaderDescriptionExample
x-bgt-hmac-sha256HMAC signature for payload verificationa1b2c3d4...
x-bgt-delivery-timeServer-side timestamp of when the event was sentSun, 28 Sep 2025 23:41:01 GMT
Content-TypeAlways application/jsonapplication/json

Important Considerations

Scope

  • Webhooks fire for all referrals/results in your organisation
  • Currently includes only API-created orders
  • Future updates will include UI and store integration orders

API Limitations

  • The API currently only returns referrals/results created via the API
  • Future updates will allow querying all referrals/results regardless of creation method

Idempotency

Handle duplicate webhook deliveries gracefully. Use the eventUuid to ensure you only process each event once:

const processedEvents = new Set(); // Or use a database

app.post("/webhooks/bgt", async (req, res) => {
const event = req.body;

// Check if already processed
if (processedEvents.has(event.eventUuid)) {
return res.status(200).send("Already processed");
}

// Process event
await handleEvent(event);

// Mark as processed
processedEvents.add(event.eventUuid);

res.status(200).send("OK");
});

Event Payloads

All webhook events follow a consistent structure with an event wrapper containing the data payload.

Base Structure

{
"eventName": "event.type",
"eventUuid": "unique-event-id",
"version": "1.0",
"data": {
/* Event-specific data */
}
}
Upcoming Changes

Future versions will include:

  • Metadata from referrals in result payloads for easier correlation
  • Additional event types
  • Enhanced payload information

Referral Events

Fired when referrals are created or updated.

referral.created

{
"eventName": "referral.created",
"eventUuid": "98603d91-9d1b-4607-8ecb-705b33c66ef0",
"version": "1.0",
"data": {
"uuid": "b69ff810-0823-41ee-8fd2-43ff01329fae",
"status": "ISSUED",
"issuedAt": "2025-09-17T05:35:20.942Z",
"metadata": null,
"createdAt": "2025-09-17T05:35:15.810Z",
"reference": "DBK4EM3H",
"updatedAt": "2025-09-17T05:35:20.942Z",
"profileUuid": null,
"organisationUuid": "dbea642f-b741-4d33-ba7b-10fd0229a961"
}
}

referral.updated

Same structure as referral.created with updated field values.

Referral Statuses

StatusDescription
PENDING_VERIFICATIONProfile requires completion before referral can be issued
CREATEDReferral has been created but not yet issued
ISSUEDReferral has been issued and sent to the patient
SCHEDULED_REISSUEReferral is scheduled to be reissued to another customer
PARTIAL_RESULTSPartial results available for this referral (can occur multiple times)
COMPLETEAll results are completed for this referral
INSUFFICIENT_DATAUnable to proceed with completion due to insufficient data
REFUNDEDReferral has been refunded
FAILED_TESTTest failed and a new referral is required
BILLING_FAILEDBilling failed for the REST API request
TRANSFERREDReferral was transferred to another person

Result Events

Fired when test results are created or updated.

result.created

{
"eventName": "result.created",
"eventUuid": "fd618482-7463-49dd-baa7-4289c62b0139",
"version": "1.0",
"data": {
"uuid": "89dd5014-750c-48c2-8f39-c39a34a4a637",
"status": "CREATED",
"metadata": null,
"createdAt": "2025-09-17T08:01:27.616Z",
"updatedAt": "2025-09-17T08:01:27.641Z",
"profileUuid": null,
"referralUuid": "ffc14dd3-7216-4f52-900f-360733183142",
"organisationUuid": "dbea642f-b741-4d33-ba7b-10fd0229a961"
}
}

result.updated

Same structure as result.created with updated field values. This event fires when:

  • Biomarker data becomes available
  • Result status changes
  • Results are reviewed or finalized

Result Statuses

StatusDescription
CREATEDResult record created, awaiting data
IN_REVIEWResults are in review with the organisation, pending release
PARTIAL_RESULTSPartial results are available (can occur multiple times)
COMPLETEAll results finalized and available

Retrieving Full Data

Webhook payloads contain minimal data. To get complete referral or result details including biomarkers:

async function handleResultUpdated(event) {
const resultUuid = event.data.uuid;

// Fetch full result data from API
const response = await fetch(
`https://api.eirly.health/v1/results/${resultUuid}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);

const fullResult = await response.json();
// fullResult includes biomarkers array
}

Testing Webhooks

Local Development

Use tools like ngrok to expose your local server:

ngrok http 3000

Use the generated HTTPS URL as your webhook endpoint during development.

Webhook Logs

Monitor webhook delivery in your Eirly organisation settings. Logs show:

  • Delivery attempts
  • Response codes
  • Retry status
  • Failure reasons

Best Practices

  1. Respond quickly - Acknowledge receipt immediately (200 OK), then process asynchronously
  2. Verify signatures - Always validate the HMAC signature in production
  3. Handle duplicates - Use eventUuid for idempotency
  4. Implement retries - Handle transient failures gracefully
  5. Monitor failures - Set up alerts for webhook delivery failures
  6. Use HTTPS - Encrypt webhook data in transit
  7. Log events - Keep audit logs of received webhooks

Example Implementation

Complete webhook handler example:

const express = require("express");
const crypto = require("crypto");

const app = express();

// Use raw body for signature verification
app.post(
"/webhooks/bgt",
express.raw({ type: "application/json" }),
async (req, res) => {
try {
// Verify signature
const signature = req.headers["x-bgt-hmac-sha256"];
const payload = req.body.toString();

if (!verifySignature(payload, process.env.BGT_SECRET, signature)) {
return res.status(401).json({ error: "Invalid signature" });
}

// Parse event
const event = JSON.parse(payload);

// Check for duplicates
if (await isProcessed(event.eventUuid)) {
return res.status(200).json({ message: "Already processed" });
}

// Respond immediately
res.status(200).json({ message: "Received" });

// Process asynchronously
processEventAsync(event).catch(console.error);
} catch (error) {
console.error("Webhook error:", error);
res.status(500).json({ error: "Processing failed" });
}
}
);

async function processEventAsync(event) {
// Mark as processing
await markProcessing(event.eventUuid);

try {
switch (event.eventName) {
case "referral.created":
await handleReferralCreated(event.data);
break;
case "result.updated":
await handleResultUpdated(event.data);
break;
// ... other events
}

// Mark as complete
await markComplete(event.eventUuid);
} catch (error) {
await markFailed(event.eventUuid, error);
}
}

function verifySignature(payload, secret, signature) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");

return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}

Troubleshooting

Common Issues

Webhooks not being received:

  • Verify your endpoint is publicly accessible
  • Check firewall/security group settings
  • Ensure endpoint returns 2xx status code
  • Verify HTTPS certificate is valid

Webhooks timing out:

  • Respond within 3 seconds
  • Move heavy processing to background jobs
  • Return 200 OK immediately

Duplicate events:

  • Implement idempotency using eventUuid
  • Check retry logic isn't causing duplicates

Support

For webhook issues:

  • Check webhook logs in your organisation settings
  • Review your endpoint's error logs
  • Contact Eirly API support with the eventUuid