Skip to main content

Webhooks

Configure your webhook URL and JustLabs will deliver these events as orders progress:

EventFires when
order.requisition_readyThe order's status becomes requisition_ready.
order.results_readyThe order's status becomes results_ready.
order.cancelledA partner cancels the order via DELETE /orders/{id}. This is the only source of the event today — a cancellation arranged with JustLabs support does not emit it.

Payload

Deliberately minimal — ids only, never patient data or test names (this is a data-minimization choice: your system already has the order and should fetch fresh details via GET /orders/{id} rather than trust a cached payload):

{
"id": "order_123__order.results_ready__<trigger>",
"type": "order.results_ready",
"createdAt": "2026-09-10T14:32:00.000Z",
"data": {
"orderId": "order_123",
"externalId": "ext-4711",
"status": "results_ready"
}
}

Headers

HeaderPurpose
Content-Typeapplication/json
X-JustLabs-EventThe event type, e.g. order.results_ready.
X-JustLabs-DeliveryA unique, stable delivery id. Use this to dedupe — see "At-least-once" below.
X-JustLabs-Signaturet=<unix seconds>,v1=<hex hmac> — see verification below.
User-AgentJustLabs-Webhooks/1

Your endpoint must respond 2xx directly. JustLabs sends the request with redirects disabled — a 3xx response is never followed and counts as a failed delivery attempt, exactly like a 4xx/5xx or a timeout. This is deliberate: a signed webhook body must never be silently re-POSTed to a different origin. Point the configured URL directly at the handler, with no redirect in between.

At-least-once delivery

Webhooks are at-least-once, not exactly-once. The same event can arrive more than once (e.g. your endpoint returned a 2xx but the response was lost in transit, or an internal retry re-fires the same trigger). Dedupe on X-JustLabs-Delivery — treat a delivery id you've already processed as a no-op success, and always return 200 for it. Do not rely on receiving each event exactly once, and there is no ordering guarantee, even for deliveries on the same order — the first attempt happens inline at the triggering event, while retries run on a cron with roughly 5-minute granularity, so two deliveries for the same order can arrive out of the order their underlying state changes happened in. Don't infer order state purely from delivery arrival order: treat data.status as a hint for what changed, and re-fetch GET /orders/{id} for the authoritative current state if your logic depends on it.

Retry schedule

A non-2xx response (including a 3xx redirect — see above) or a timeout (JustLabs waits up to 10 seconds) is retried on this backoff, in minutes after the previous attempt:

1, 5, 30, 120, 360, 720, 1440, 1440

(1 min, 5 min, 30 min, 2 hr, 6 hr, 12 hr, 1 day, 1 day — 8 attempts total). The last 1440 is the wait before the 8th attempt; that 8th attempt is not itself followed by another wait, so the retry window spans the sum of the first 7 gaps — about 45 hours, not a full 2 days — plus up to ~5 minutes of extra latency per retry from the cron's own polling granularity. After the final (8th) attempt fails, the delivery is marked failed and JustLabs support is alerted; you can still poll GET /orders/{id} at any time regardless of webhook delivery state — polling is always available as a fallback.

Verifying the signature

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(secret, header, rawBody, now = Date.now()) {
if (!header) return false;
const p = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
if (Math.abs(now - Number(p.t) * 1000) > 300_000) return false;
const expected = createHmac('sha256', secret).update(`${p.t}.${rawBody}`).digest('hex');
return expected.length === p.v1.length && timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(p.v1, 'hex'));
}

Usage in an Express-style handler (verify against the raw, unparsed request body — signing is computed over the exact bytes sent):

app.post('/webhooks/justlabs', express.raw({ type: 'application/json' }), (req, res) => {
// req.header(...) returns undefined when the header is absent — verify()
// guards against that (and any other malformed header) internally.
const ok = verify(process.env.JUSTLABS_WEBHOOK_SECRET, req.header('X-JustLabs-Signature'), req.body.toString('utf8'));
if (!ok) return res.status(401).end();
let event;
try {
event = JSON.parse(req.body);
} catch {
return res.status(400).end();
}
// ... dedupe on req.header('X-JustLabs-Delivery'), then handle event ...
res.status(200).end();
});

The tolerance window is 5 minutes — reject anything older (protects against replay) or with a timestamp too far in the future (clock skew / forged header).