Webhooks
Webhooks push fulfilment events to your server as they happen, so you do not have to poll /orders or /units. Each delivery is signed with HMAC-SHA256 using a secret you choose, so you can verify it genuinely came from GlycanAge.
Registering an endpoint
POST /webhooks
Content-Type: application/json
{
"url": "https://your-domain.com/webhooks/glycanage",
"events": ["order.shipped", "unit.registered", "result.ready"],
"secret": "a-secret-of-at-least-32-characters-long"
}
url— must be anhttps://URL.events— one or more oforder.shipped,unit.registered,result.ready.secret— 32 to 128 characters, used to sign every delivery. Generate it randomly and store it alongside your API key.
GET /webhooks
DELETE /webhooks/{id}
Delivery format
Every event is an HTTP POST to your URL with this body:
{
"event": "order.shipped",
"timestamp": "2026-07-06T09:21:00.000Z",
"data": {
"order_id": "4f3c2a4e-1d2b-4f6a-9c8e-2b1a3c4d5e6f",
"reference": "SHOP-10231",
"kits": [
{
"kit_code": "GA-AB-123456",
"qr_link": "https://qr.glycanage.com/9f8e7d6c5b4a3210",
"tracking_code": "TRK123456"
}
]
}
}
Two headers accompany each delivery:
| Header | Contents |
|---|---|
X-Webhook-Event | The event type, mirroring event in the body. |
X-Webhook-Signature | HMAC SHA-256 of the raw request body, keyed with your secret, lowercase hex. |
Verifying the signature
Compute the HMAC over the exact bytes you received, before JSON parsing — re-serialising the parsed object will not reproduce the same digest.
const crypto = require("crypto");
// Capture the raw body: express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(signature ?? "", "utf8");
const b = Buffer.from(expected, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hmac, hashlib
def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature or "", expected)
Reject anything that fails verification with a 401, and never act on an unverified payload.
Events
order.shipped
The warehouse has dispatched the order. This is where you learn the kit codes.
| Field | Type |
|---|---|
order_id | string |
reference | string | null |
kits | array of { kit_code, qr_link, tracking_code } |
qr_link is the page your customer uses to register the kit. It is printed on the kit, but having it here lets you include it in your own shipping confirmation email.
unit.registered
The customer has completed the QR page for one kit. Analysis can now proceed.
| Field | Type |
|---|---|
order_id | string |
reference | string | null |
kit_code | string |
result.ready
A report has been released. This is a status signal only — it carries no result data.
| Field | Type |
|---|---|
order_id | string |
reference | string | null |
kit_code | string |
result_id | string |
Fetch the report itself from GET /reports/{result_id} or GET /reports/kit/{kitCode} — as JSON, or as a PDF with Accept: application/pdf. This is also the point at which an AI interpretation becomes available for the kit.
Sandbox simulation
In the sandbox the whole lifecycle is simulated and driven by a single action: placing an order. Over the following seconds it fires
order.shipped → unit.registered → result.ready
with generated but structurally valid kit codes, QR links, tracking and result values. A placed order reaches result.ready in roughly half a minute, so you can exercise all three handlers in one run.
Unlike the Partner API, you do not need to take a second action to advance the lab stages — there is no assignment step, because your customers register their own kits.
This simulation is only active in the sandbox. In production these events reflect real warehouse, laboratory and analysis activity.
Handling deliveries well
Respond quickly. Acknowledge with 200 within 10 seconds. Do the real work asynchronously.
Be idempotent. An event may be delivered more than once. Key on something stable — result_id for result.ready, kit_code for unit.registered, order_id for order.shipped — and make reprocessing a no-op.
app.post("/webhooks/glycanage", (req, res) => {
if (!verifySignature(req.rawBody, req.get("x-webhook-signature"), SECRET)) {
return res.status(401).send("Unauthorized");
}
const { event, data } = req.body;
const key = `${event}:${data.result_id ?? data.kit_code ?? data.order_id}`;
if (alreadyHandled(key)) return res.status(200).send("OK");
enqueue(event, data); // process out of band
markHandled(key);
res.status(200).send("OK");
});
Do not depend on ordering. Events for different kits on the same order can arrive in any order.
Keep a fallback. Webhook delivery is best-effort. For anything you must not miss, reconcile periodically against GET /units or GET /reports — results_ready on a kit tells you whether a report exists regardless of whether its webhook arrived.
Testing
Expose a local endpoint with ngrok and register the public URL against the sandbox:
ngrok http 3000
{
"url": "https://abc123.ngrok.io/webhooks/glycanage",
"events": ["order.shipped", "unit.registered", "result.ready"],
"secret": "test-secret-key-at-least-32-characters"
}
Then place a sandbox order and watch all three events arrive.
Troubleshooting
Nothing arrives — check the webhook is registered against the same environment you are ordering in (sandbox registrations do not receive production events), that your URL is https:// and reachable from the internet, and that you return 200.
Signatures never match — you are almost certainly hashing a re-serialised body. Capture and hash the raw bytes.
Events stop mid-flow — in production a kit that is never registered by the customer will never produce result.ready. Check registered on GET /units/kit/{kitCode}.
For anything else, contact support@glycanage.com with your reseller name, the affected order reference and any error logs.