Webhooks
Webhooks allow you to receive real-time notifications about key events in your GlycanAge integration. Instead of polling our API for status updates, webhooks will automatically notify your system when important events occur, such as when reports are ready or order statuses change.
Overview
Our webhook system sends HTTP POST requests to your specified endpoint whenever subscribed events occur. Each webhook includes event data and is secured using HMAC authentication to ensure the requests are genuinely from GlycanAge.
Setting Up Webhooks
1. Create a Webhook Endpoint
First, create an endpoint on your server that can receive HTTP POST requests. This endpoint will receive webhook notifications from GlycanAge.
// Example webhook endpoint (Node.js/Express)
app.post('/webhooks/glycanage', (req, res) => {
const signature = req.headers['x-glycanage-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.type);
// Respond with 200 to acknowledge receipt
res.status(200).send('OK');
} else {
res.status(401).send('Unauthorized');
}
});
2. Register Your Webhook
Use the API to register your webhook endpoint:
POST /webhooks
Authorization: Basic {base64_encoded_email:token}
Content-Type: application/json
{
"name": "My Integration Webhook",
"description": "Webhook for processing GlycanAge events",
"url": "https://your-domain.com/webhooks/glycanage",
"events": [
"order-status-changed",
"assignment-status-changed",
"kit-received",
"report-ready"
],
"secret": "your-secure-32-character-or-longer-secret"
}
Authentication
All webhook requests are authenticated using HMAC-SHA256. We compute a signature using your provided secret and include it in the X-GlycanAge-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 signature === `sha256=${expectedSignature}`;
}
import hmac
import hashlib
def verify_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return signature == f"sha256={expected_signature}"
Event Types
1. Order Status Changed
Triggered when the status of a kit order or bulk order changes (e.g., pending → paid, pending → approved).
{
"type": "order-status-changed",
"timestamp": 1693123200000,
"data": {
"orderId": "order_12345",
"orderType": "bulk-order", // or "kit-order"
"previousStatus": "pending",
"currentStatus": "paid",
"practiceId": "practice_67890",
"numberOfKits": 50,
"amount": 25000, // in cents
"currency": "USD"
}
}
When this event is triggered:
- Kit order approval/rejection
- Payment status changes (pending → paid, paid → failed)
- Order completion
2. Assignment Status Changed
Triggered when the status of an assignment changes (e.g., pending → approved, pending → paid).
{
"type": "assignment-status-changed",
"timestamp": 1693123200000,
"data": {
"assignmentId": "assignment_12345",
"previousStatus": "pending",
"currentStatus": "approved",
"practiceId": "practice_67890",
"paymentType": "bank-transfer", // or "token"
"amount": 15000, // in cents (null if paid with tokens)
"currency": "USD",
"sets": [
{
"patientId": "patient_123",
"kitCode": "KIT001234",
"product": "regular"
}
]
}
}
When this event is triggered:
- Assignment approval/rejection
- Payment processing completion
- Assignment creation with token payment
3. Kit Received
Triggered when a physical kit is received and processed at the GlycanAge laboratory.
{
"type": "kit-received",
"timestamp": 1693123200000,
"data": {
"kitCode": "KIT001234",
"testId": "test_56789",
"patientId": "patient_123",
"practiceId": "practice_67890",
"assignmentId": "assignment_12345",
"receivedAt": 1693120000000,
"status": "inAnalysis"
}
}
When this event is triggered:
- Kit arrives at laboratory
- Kit passes initial quality checks
- Processing begins
4. Report Ready
Triggered when a glycan analysis is complete and the report is available for download.
{
"type": "report-ready",
"timestamp": 1693123200000,
"data": {
"reportId": "report_78901",
"testId": "test_56789",
"kitCode": "KIT001234",
"patientId": "patient_123",
"practiceId": "practice_67890",
"assignmentId": "assignment_12345",
"glycanAge": "45.2",
"chronologicalAge": "42",
"reportUrl": "/reports/report_78901",
"createdOn": 1693123000000
}
}
When this event is triggered:
- Laboratory analysis is complete
- Report generation is finished
- Quality assurance checks pass
Webhook Management
List Your Webhooks
GET /webhooks
Authorization: Basic {base64_encoded_email:token}
Delete a Webhook
DELETE /webhooks/{webhookId}
Authorization: Basic {base64_encoded_email:token}
Best Practices
1. Respond Quickly
Your webhook endpoint should respond with a 200 status code within 10 seconds. We'll retry failed deliveries up to 3 times with exponential backoff.
2. Handle Duplicate Events
Implement idempotency in your webhook handler. Events may occasionally be delivered more than once.
const processedEvents = new Set();
app.post('/webhooks/glycanage', (req, res) => {
const eventId = req.body.id || `${req.body.type}-${req.body.timestamp}`;
if (processedEvents.has(eventId)) {
return res.status(200).send('Already processed');
}
// Process the event
processEvent(req.body);
processedEvents.add(eventId);
res.status(200).send('OK');
});
3. Validate Event Data
Always validate the structure and content of webhook events before processing:
function validateEvent(event) {
if (!event.type || !event.timestamp || !event.data) {
throw new Error('Invalid event structure');
}
const validTypes = [
'order-status-changed',
'assignment-status-changed',
'kit-received',
'report-ready'
];
if (!validTypes.includes(event.type)) {
throw new Error('Unknown event type');
}
return true;
}
4. Handle Errors Gracefully
If your webhook endpoint returns a non-200 status code, we'll retry the delivery. Make sure to handle errors appropriately:
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 if it's a validation error to prevent retries
if (error.message.includes('Invalid') || error.message.includes('Unknown')) {
res.status(200).send('Invalid event');
} else {
// Return 500 for processing errors to trigger retries
res.status(500).send('Processing failed');
}
}
});
Testing Webhooks
For testing purposes, you can use tools like:
- ngrok: Expose your local development server to the internet
- webhook.site: Generate temporary webhook URLs for testing
- Postman: 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": ["report-ready"],
"secret": "test-secret-key-for-development"
} -
Test with sample data to ensure your handler works correctly.
Troubleshooting
Common Issues
Webhook not receiving events:
- Verify webhook is approved by GlycanAge team
- Check that your endpoint returns 200 status codes
- Ensure your URL is accessible from the internet
Authentication failures:
- Verify your HMAC signature verification logic
- Check that you're using the correct secret
- Ensure you're computing the signature on the raw request body
Missed events:
- Implement proper error handling and logging
- Check webhook delivery logs (contact support for access)
- Consider implementing a fallback polling mechanism for critical events
For additional support, contact support@glycanage.com with your webhook configuration and any error logs.