Webhooks
Webhooks tell you about key events in your GlycanAge integration as they happen, so you don't have to keep polling our API for status updates.
Overview
When an event you've subscribed to happens, we send an HTTP POST to the endpoint you gave us. Every delivery carries the event data and an HMAC signature, so you can confirm the request really came from GlycanAge.
Sandbox Simulation
The sandbox simulates the ordering and lab flow, so you can test your webhook integration end to end without physical kits or anyone processing them by hand. Your own actions drive the simulation, the same way they drive the real thing:
-
Place an order. It ships automatically, with no manual approval, and
partner_order.shippedfires with generated kit codes and tracking links. -
Assign kits to a patient. Creating an assignment walks those kits through the lab lifecycle:
unit.in_lab → unit.in_analysis → result.ready
Placing an order on its own won't produce results. You have to assign its kits to a patient before anything moves through the lab stages.
- Kits, tracking codes and reports come back with fake but structurally valid data.
- Each step fires after a short delay, so you can watch the transitions happen.
None of this happens outside the sandbox. In production these events reflect real warehouse, laboratory and analysis activity.
Setting Up Webhooks
1. Create a Webhook Endpoint
Start with an endpoint on your server that accepts HTTP POST requests. It has to be reachable over HTTPS.
// Example webhook endpoint (Node.js/Express)
app.post("/webhooks/glycanage", (req, res) => {
const signature = req.headers["x-webhook-signature"];
const payload = JSON.stringify(req.body);
// Verify the webhook signature (see authentication section)
if (verifySignature(payload, signature, webhookSecret)) {
const event = req.body;
// Process the event
console.log("Received event:", event.event);
// Respond with 200 to acknowledge receipt
res.status(200).send("OK");
} else {
res.status(401).send("Unauthorized");
}
});
2. Register Your Webhook
Now register that endpoint with us:
POST /webhooks
Authorization: Basic {base64_encoded_token}
Content-Type: application/json
{
"url": "https://your-domain.com/webhooks/glycanage",
"events": [
"partner_order.shipped",
"unit.in_lab",
"unit.in_analysis",
"result.ready"
],
"secret": "your-secure-32-character-or-longer-secret"
}
urlhas to be an HTTPS URL.eventsis an array of 1 to 4 event types you want to subscribe to.secretis a string of 32 to 128 characters, used to sign each delivery so you can verify it.
Authentication
We sign every webhook request with HMAC-SHA256, using the secret you gave us, and send the result as a hex digest in the X-Webhook-Signature header.
Verifying Webhook Signatures
const crypto = require("crypto");
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Webhook Payload
Every payload has the same shape:
{
"event": "<event_type>",
"timestamp": "2024-08-27T12:00:00.000Z",
"data": { ... }
}
eventis the event type string.timestampis an ISO 8601 timestamp of when the event fired.dataholds the event-specific payload, which varies by type.
You also get the event type in the X-Webhook-Event header.
Event Types
partner_order.shipped
Fires when a partner order leaves the warehouse.
{
"event": "partner_order.shipped",
"timestamp": "2024-08-27T12:00:00.000Z",
"data": {
"order_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"kits": [
{
"kitCode": "KIT001234",
"trackingCode": "1Z999AA10123456784",
"trackingLink": "https://tracking.example.com/1Z999AA10123456784"
}
]
}
}
unit.in_lab
Fires when a kit arrives at the GlycanAge laboratory.
This works in the sandbox today, where the simulation fires it when you assign kits. It isn't live in production yet, but it will be in a future release.
unit.in_analysis
Fires when a kit enters the analysis phase at the laboratory.
This works in the sandbox today, where the simulation fires it when you assign kits. It isn't live in production yet, but it will be in a future release.
result.ready
Fires when the glycan analysis is finished and the report is available.
This works in the sandbox today, where the simulation fires it when you assign kits. It isn't live in production yet, but it will be in a future release.
Webhook Management
List Your Webhooks
GET /webhooks
Authorization: Basic {base64_encoded_token}
Delete a Webhook
DELETE /webhooks/{webhookId}
Authorization: Basic {base64_encoded_token}
Best Practices
1. Respond Quickly
Answer with a 200 within 10 seconds. Do the real work afterwards, out of band.
2. Handle Duplicate Events
An event can occasionally be delivered more than once, so make your handler idempotent.
const processedEvents = new Set();
app.post("/webhooks/glycanage", (req, res) => {
const eventId = `${req.body.event}-${req.body.timestamp}`;
if (processedEvents.has(eventId)) {
return res.status(200).send("Already processed");
}
processEvent(req.body);
processedEvents.add(eventId);
res.status(200).send("OK");
});
3. Validate Event Data
Check the shape and contents of an event before you act on it:
function validateEvent(event) {
if (!event.event || !event.timestamp || !event.data) {
throw new Error("Invalid event structure");
}
const validTypes = [
"partner_order.shipped",
"unit.in_lab",
"unit.in_analysis",
"result.ready",
];
if (!validTypes.includes(event.event)) {
throw new Error("Unknown event type");
}
return true;
}
4. Handle Errors Gracefully
app.post("/webhooks/glycanage", async (req, res) => {
try {
const event = req.body;
validateEvent(event);
await processEvent(event);
res.status(200).send("OK");
} catch (error) {
console.error("Webhook processing error:", error);
// Return 200 for validation errors to prevent retries
if (
error.message.includes("Invalid") ||
error.message.includes("Unknown")
) {
res.status(200).send("Invalid event");
} else {
res.status(500).send("Processing failed");
}
}
});
Testing Webhooks
A few tools that help while you're building:
- ngrok exposes your local development server to the internet
- webhook.site gives you a throwaway webhook URL
- Postman lets you mock webhook requests with sample payloads
Example Test Setup
-
Use ngrok to expose your local webhook endpoint:
ngrok http 3000 -
Register the ngrok URL as your webhook endpoint:
{"url": "https://abc123.ngrok.io/webhooks/glycanage","events": ["partner_order.shipped"],"secret": "test-secret-key-for-development-env"} -
Send some sample data through and check your handler does the right thing.
Troubleshooting
Common Issues
Nothing is arriving:
- Check the webhook has been approved by our team
- Check your endpoint actually returns a 200
- Check your URL is reachable from the internet
Signatures never match:
- Go over your HMAC verification logic
- Make sure you're using the right secret
- Make sure you're hashing the raw request body, not a re-serialised version of it
Events go missing:
- Add proper error handling and logging
- Ask us for your webhook delivery logs
- Fall back to polling for anything you really can't afford to miss
If you're still stuck, email support@glycanage.com with your webhook configuration and any error logs.