Webhooks
Subscribe to events on the Hygrade platform and we’ll push them to your HTTPS endpoint as they happen. Build near-real-time order dashboards, sync shipping updates into your ERP, and kick off downstream workflows without polling.
How webhooks work
- Register an HTTPS endpoint in the Hygrade Developer Portal and select the events you care about.
- When an event occurs, Hygrade sends a signed
POSTto your endpoint with the event payload. - Your endpoint verifies the signature, processes the event, and returns
2xxwithin 10 seconds. - Non-2xx responses and timeouts trigger automatic retries.
Event types
| Event | Fires when |
|---|---|
order.submitted | A shopper submits a new order. Fires before approval routing. |
order.approval.requested | An order enters the approval queue of a named approver. |
order.approved | An order clears its full approval chain. |
order.rejected | An approver rejects the order; includes the rejection reason. |
order.in_production | Production starts on the order. |
order.shipped | Order ships; payload includes carrier and tracking number. |
order.delivered | Carrier confirms delivery. |
order.cancelled | Order is cancelled. Includes the cancelling user and reason. |
user.created | A new shopper record is created (SAML JIT or SCIM). |
user.deactivated | A shopper is deactivated. |
catalog.item.updated | A catalog item’s pricing, availability, or description changes. |
Event payload
Every event shares a common envelope. The data object mirrors the REST API shape for the resource.
{
"id": "evt_7d2c1b94e8fa3042",
"object": "event",
"type": "order.shipped",
"created_at": "2026-04-15T14:02:17Z",
"cust_id": "ACME001",
"api_version": "v1",
"data": {
"id": "ord_8c3e47a9b1f240d6",
"object": "order",
"status": "shipped",
"shipment": {
"carrier": "UPS",
"tracking_number": "1Z999AA10123456784",
"shipped_at": "2026-04-15T14:01:50Z",
"estimated_delivery": "2026-04-18"
}
}
}
Signature verification
Every webhook request includes a signature header. Verify it before trusting the payload — anything your endpoint can receive on the open internet could be forged without this check.
POST /webhooks/hygrade HTTP/1.1
Host: hooks.acmecorp.com
Content-Type: application/json
Hygrade-Event: order.shipped
Hygrade-Delivery: del_93f0c2a7d4e1b82c
Hygrade-Signature: t=1760731337,v1=5f3a...,v1=8b0e...
User-Agent: Hygrade-Webhooks/1.0
The Hygrade-Signature header contains a timestamp and one or more HMAC-SHA256 signatures, each computed as:
signed_payload = timestamp + "." + raw_request_body
signature = HMAC_SHA256(signing_secret, signed_payload)
Multiple v1=... entries are provided during secret rotation so a single old and new secret are both accepted for up to 24 hours.
Node.js example
import crypto from 'crypto';
export function verifyHygradeSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=').map((s) => s.trim()))
);
const timestamp = parts.t;
const sent = header.match(/v1=([a-f0-9]+)/g).map((s) => s.slice(3));
if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
throw new Error('timestamp outside allowed window');
}
const signed = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signed)
.digest('hex');
const ok = sent.some((s) =>
crypto.timingSafeEqual(Buffer.from(s, 'hex'), Buffer.from(expected, 'hex'))
);
if (!ok) throw new Error('signature mismatch');
}
PHP example
function verifyHygradeSignature(string $rawBody, string $header, string $secret): void {
$pairs = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_map('trim', explode('=', $part, 2));
$pairs[$k][] = $v;
}
$timestamp = $pairs['t'][0] ?? null;
$signatures = $pairs['v1'] ?? [];
if (!$timestamp || abs(time() - (int) $timestamp) > 300) {
throw new RuntimeException('timestamp outside allowed window');
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
foreach ($signatures as $sig) {
if (hash_equals($expected, $sig)) return;
}
throw new RuntimeException('signature mismatch');
}
Retries
If your endpoint returns a non-2xx status or times out (10-second budget), Hygrade retries with exponential backoff:
- First retry: 30 seconds
- Second: 2 minutes
- Third: 10 minutes
- Subsequent: every hour, for up to 24 hours
After 24 hours of failure, the delivery is marked failed and visible in the Developer Portal’s Deliveries view, where you can replay it manually after fixing the issue on your end.
Idempotency
Retries reuse the same Hygrade-Delivery ID. Key your idempotency store on this ID so that a retry doesn’t double-book an order into your ERP.
Ordering and duplicates
- At-least-once delivery — design your handler to tolerate duplicates.
- Events are not strictly ordered. Use the
created_attimestamp and the resource’sstatusto reconcile — for example, ignore anorder.approvedevent if you’ve already processedorder.shippedfor the same order.
Managing subscriptions
Webhooks are managed through the Developer Portal → Webhooks screen. You can also manage them via the REST API:
curl -X POST https://api.hygradebusiness.com/v1/webhooks \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.acmecorp.com/webhooks/hygrade",
"events": ["order.approved", "order.shipped", "order.delivered"],
"description": "ERP order sync"
}'
{
"id": "whs_3b8c9e04",
"object": "webhook_subscription",
"url": "https://hooks.acmecorp.com/webhooks/hygrade",
"events": ["order.approved", "order.shipped", "order.delivered"],
"status": "enabled",
"signing_secret": "whsec_d7e42...",
"created_at": "2026-04-15T14:00:00Z"
}
The signing_secret is returned once, on creation. Store it securely; Hygrade cannot recover it for you. If lost, rotate with POST /v1/webhooks/{id}/rotate_secret.
Testing locally
During development, expose your local endpoint with a tunneling tool (ngrok, Cloudflare Tunnel, tailscale funnel) and point the webhook at the public URL. Send a test event at any time from the Developer Portal or via:
Next: explore available event data in the REST API reference, or return to the integration overview.