Platform · Preview

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

  1. Register an HTTPS endpoint in the Hygrade Developer Portal and select the events you care about.
  2. When an event occurs, Hygrade sends a signed POST to your endpoint with the event payload.
  3. Your endpoint verifies the signature, processes the event, and returns 2xx within 10 seconds.
  4. Non-2xx responses and timeouts trigger automatic retries.

Event types

EventFires when
order.submittedA shopper submits a new order. Fires before approval routing.
order.approval.requestedAn order enters the approval queue of a named approver.
order.approvedAn order clears its full approval chain.
order.rejectedAn approver rejects the order; includes the rejection reason.
order.in_productionProduction starts on the order.
order.shippedOrder ships; payload includes carrier and tracking number.
order.deliveredCarrier confirms delivery.
order.cancelledOrder is cancelled. Includes the cancelling user and reason.
user.createdA new shopper record is created (SAML JIT or SCIM).
user.deactivatedA shopper is deactivated.
catalog.item.updatedA 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.

POST — your endpoint
{
  "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.

Request headers
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:

Signature algorithm
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

verify.js
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

verify.php
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:

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

Managing subscriptions

Webhooks are managed through the Developer Portal → Webhooks screen. You can also manage them via the REST API:

GET /v1/webhooks
POST /v1/webhooks
PATCH /v1/webhooks/{id}
DELETE /v1/webhooks/{id}
Create a subscription
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"
  }'
201 Created
{
  "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:

POST /v1/webhooks/{id}/test

Next: explore available event data in the REST API reference, or return to the integration overview.