Skip to main content

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 an https:// URL.
  • events — one or more of order.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:

HeaderContents
X-Webhook-EventThe event type, mirroring event in the body.
X-Webhook-SignatureHMAC 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.

FieldType
order_idstring
referencestring | null
kitsarray 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.

FieldType
order_idstring
referencestring | null
kit_codestring

result.ready

A report has been released. This is a status signal only — it carries no result data.

FieldType
order_idstring
referencestring | null
kit_codestring
result_idstring

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 /reportsresults_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.