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 createdreferral.updated- Fired when a referral is updatedreferral.billing_failed- Fired when the billing failed for the REST api requestreferral.pending_verification- Fired when the profile requires completion before referral can be issuedreferral.complete- All results are completed for this referralreferral.insufficient_data- Unable to proceed with completion on this referralreferral.refunded- If the referral was refundedreferral.failed_test- If a test on the referral failed and resulted in a new referral being requiredreferral.partial_results- We have partial results for this referral, can be fired multiple times.referral.transferred- If the original referral was transferred to another personreferral.issued- The referral has been issued to the customerreferral.scheduled_reissue- The referral is about to be reissued to another customerresult.created- Fired when a result is createdresult.updated- Fired when a result is updatedresult.complete- All results completedresult.in_review- The results are in review with the org, and will be released upon reviewresult.partial_results- We have partial results for this result.
Setup
Configure webhooks in your organisation settings page: Organisation => Settings => Api Keys

To configure a webhook:
- Navigate to Organisation Settings > API Keys
- Enter your webhook endpoint URL
- Select the events you want to receive
- 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.createdbeforereferral.updatedfor the same referral - You will always receive
result.createdbeforeresult.updatedfor the same result
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:
| Attempt | Delay | Formula |
|---|---|---|
| 1 | Immediate | Initial attempt |
| 2 | 4 minutes | 2² minutes |
| 3 | 9 minutes | 3² minutes |
| 4 | 16 minutes | 4² minutes |
| 5 | 25 minutes | 5² 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.
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:
| Header | Description | Example |
|---|---|---|
x-bgt-hmac-sha256 | HMAC signature for payload verification | a1b2c3d4... |
x-bgt-delivery-time | Server-side timestamp of when the event was sent | Sun, 28 Sep 2025 23:41:01 GMT |
Content-Type | Always application/json | application/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 */
}
}
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
| Status | Description |
|---|---|
PENDING_VERIFICATION | Profile requires completion before referral can be issued |
CREATED | Referral has been created but not yet issued |
ISSUED | Referral has been issued and sent to the patient |
SCHEDULED_REISSUE | Referral is scheduled to be reissued to another customer |
PARTIAL_RESULTS | Partial results available for this referral (can occur multiple times) |
COMPLETE | All results are completed for this referral |
INSUFFICIENT_DATA | Unable to proceed with completion due to insufficient data |
REFUNDED | Referral has been refunded |
FAILED_TEST | Test failed and a new referral is required |
BILLING_FAILED | Billing failed for the REST API request |
TRANSFERRED | Referral 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
| Status | Description |
|---|---|
CREATED | Result record created, awaiting data |
IN_REVIEW | Results are in review with the organisation, pending release |
PARTIAL_RESULTS | Partial results are available (can occur multiple times) |
COMPLETE | All 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
- Respond quickly - Acknowledge receipt immediately (200 OK), then process asynchronously
- Verify signatures - Always validate the HMAC signature in production
- Handle duplicates - Use
eventUuidfor idempotency - Implement retries - Handle transient failures gracefully
- Monitor failures - Set up alerts for webhook delivery failures
- Use HTTPS - Encrypt webhook data in transit
- 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