CanopyDocs

Webhooks

Subscribe to signed events — the event catalog, signature verification, delivery and retry semantics, delivery logs, and replay.

Webhooks push events to your endpoint as they happen, so you don't have to poll. Every delivery is signed (HMAC-SHA256), retried on failure, logged, and individually replayable.

Event catalog

Subscribe an endpoint to any of these event types. The data payload for each mirrors the corresponding API resource.

Event typeFires whendata includes
order.createdAn order is ingestedorderId, orderNumber, channelOrderId, status
order.shippedAn order transitions to shippedorderId, orderNumber, trackingNumber, carrierService
shipment.createdA label is createdshipmentId, orderId, trackingNumber, carrierService, shippingCost, shipCode
purchase_order.receivedReceiving completes on a POinboundShipmentId, shipmentNumber, externalPoNumber, status
purchase_order.updatedA PO is updatedinboundShipmentId, shipmentNumber, externalPoNumber
return.completedA return passes inspectionreturnId, rmaNumber, status
return.cancelledA return is cancelledreturnId, rmaNumber, status, reason

Event-type names are a stable, additive contract — new event types are added over time without renaming existing ones, so subscribe to the specific types you need and ignore unknown ones.

Payload

Every delivery is a POST with this envelope:

Webhook body
{
  "id": "evt_clz9q2k7b0001x8p3a1b2c3d4",
  "type": "shipment.created",
  "created_at": "2026-06-29T17:42:10.123Z",
  "data": {
    "shipmentId": "shp_3c1b...",
    "orderId": "ord_9a8b...",
    "trackingNumber": "1Z999AA10123456784",
    "carrierService": "UPS Ground",
    "shippingCost": 8.42,
    "shipCode": "SHP-10231"
  }
}

Headers on every delivery:

HeaderValue
x-canopy-event-idStable id for the event (your dedupe key)
x-canopy-event-typeThe event type
x-canopy-delivery-idPer-attempt delivery handle
x-canopy-timestampUnix seconds — also part of the signature
x-canopy-signaturesha256=<base64 HMAC>

The id in the body (and x-canopy-event-id) is stable across retries and across multiple endpoints — use it to make your handler idempotent.

Verifying the signature

The signature is HMAC-SHA256 over the string `{timestamp}.{rawBody}`, base64-encoded, using your endpoint's signing secret (shown once when the endpoint is created). Verify it against the raw request body, before JSON-parsing:

Verify a webhook (Node.js)
import crypto from "node:crypto";

function verifyWebhook(rawBody, headers, secret, toleranceSeconds = 300) {
  const ts = headers["x-canopy-timestamp"];
  const sig = headers["x-canopy-signature"]; // "sha256=<base64>"

  // 1. Reject stale timestamps (replay protection)
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > toleranceSeconds) {
    return false;
  }

  // 2. Recompute over `${ts}.${rawBody}`
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("base64");

  // 3. Constant-time compare
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Bind the timestamp into your check and reject deliveries outside a tolerance window (e.g. 5 minutes) to defend against replay. Always compare signatures in constant time.

Delivery & retries

TransportPOST over HTTPS, 10-second timeout
SuccessAny 2xx response
RetriesUp to 5 attempts on non-2xx or transport error
Backoff1 min → 5 min → 30 min → 2 hr → 6 hr, then marked permanently failed
GuaranteeAt-least-once — your handler must be idempotent (use the event id)
OrderingNot guaranteed — independent retries can reorder events

Respond 2xx quickly (acknowledge, then process asynchronously). Any non-2xx — or a timeout — schedules a retry on the backoff above.

Because delivery is at-least-once and unordered, treat webhooks as a notification to go read the resource, and reconcile with updatedSince polling (see Conventions) for a guaranteed-complete picture.

Delivery logs

Every attempt is recorded. Query the recent deliveries for an endpoint:

Recent deliveries for an endpoint
curl "https://api.staging.canopywms.com/api/webhooks/endpoints/:id/deliveries" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Each row reports status (PENDING / RUNNING / SUCCEEDED / FAILED_PERMANENT), attempts, lastResponseStatus, lastError, lastAttemptAt, nextRunAt, and the originating event { id, type }.

Replay

Re-send any delivery — including a permanently-failed one — once your endpoint is healthy:

Replay a delivery
curl -X POST "https://api.staging.canopywms.com/api/webhooks/deliveries/:deliveryId/replay" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Replay re-arms the same delivery, so the body and x-canopy-event-id are identical to the original (only the timestamp and signature are recomputed).

Managing endpoints

Webhook endpoints are configured by a CanopyWMS tenant administrator (in Settings → Webhooks, or via the tenant-admin webhook API):

  • Create / update / delete endpoints, each with a URL and a list of subscribed event types. A signing secret is returned once at creation.
  • Rotate the signing secret at any time.
  • Per-client scoping — an endpoint can receive events for the whole tenant or be pinned to a single client (brand).

For security, endpoint URLs must be HTTPS and resolve to a public address — requests to private, loopback, link-local, or cloud-metadata ranges are rejected, and the target is re-validated at delivery time to defend against DNS rebinding.

On this page