Skip to main content

Webhooks

Webhooks push fulfilment events to your server as they happen, so you don't have to keep polling /orders or /units. We sign each delivery with HMAC-SHA256 using a secret you pick, so you can check it really came from us.

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 has to be an https:// URL.
  • events is one or more of order.shipped, unit.registered, result.ready.
  • secret is 32 to 128 characters and signs every delivery. Generate it randomly and keep it wherever you keep your API key.
GET /webhooks
DELETE /webhooks/{id}

Delivery format

Every event arrives as 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 come with it:

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 you parse the JSON. If you re-serialise the parsed object you won't get the same digest back.

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. Never act on a payload you haven't verified.

Events

order.shipped

The warehouse has sent the order out. This is where you find out 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's printed on the kit itself, but having it here means you can drop it into your own shipping confirmation email too.

unit.registered

The customer has finished the QR page for one kit, so analysis can go ahead.

FieldType
order_idstring
referencestring | null
kit_codestring

result.ready

A report has been released. This is a status signal only, and 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 moment an AI interpretation becomes available for the kit.

Sandbox simulation

In the sandbox the whole lifecycle is simulated, and one action sets it off: placing an order. Over the next few seconds you'll get

order.shipped → unit.registered → result.ready

with generated kit codes, QR links, tracking and result values that are fake but structurally valid. An order reaches result.ready in roughly half a minute, so you can put all three handlers through their paces in one run.

Unlike the Partner API, there's no second action needed to move the lab stages along. There's no assignment step here, because your customers register their own kits.

None of this happens outside the sandbox. In production these events reflect real warehouse, laboratory and analysis activity.

Handling deliveries well

Respond quickly. Acknowledge with 200 within 10 seconds, then 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");
});

Don't rely on ordering. Events for different kits on the same order can turn up in any order.

Keep a fallback. Webhook delivery is best-effort. For anything you can't afford to miss, reconcile every so often against GET /units or GET /reports. The results_ready flag on a kit tells you whether a report exists, whether or not its webhook ever arrived.

Testing

Expose a local endpoint with ngrok, then 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"
}

Now place a sandbox order and watch all three events land.

Troubleshooting

Nothing arrives. Check the webhook is registered in the same environment you're ordering in, since sandbox registrations don't receive production events. Then check your URL is https://, reachable from the internet, and returning 200.

Signatures never match. You're almost certainly hashing a re-serialised body. Capture and hash the raw bytes instead.

Events stop mid-flow. In production, a kit the customer never registers will never produce result.ready. Check registered on GET /units/kit/{kitCode}.

For anything else, email support@glycanage.com with your reseller name, the affected order reference and any error logs.