Skip to content
GlyphTechnologies

On-demand fabrication

Webhooks

Be told when an order is paid, shipped, or cancelled, and verify the signature.

Register an HTTPS URL and we POST to it when something happens on your account. Every delivery is signed so you can verify it came from us. Failed deliveries are retried.

Events

product.approvedA product was approved and priced. data is the product.
product.rejectedA product was rejected. data.review_note says why.
order.paidAn order was charged and its jobs created. data is the order.
order.payment_failedA charge failed. data is the order with payment.error.
order.in_productionGlyph released the order to the shop.
order.shippedShipped; data.shipment has the tracking number.
order.cancelledCancelled by you or by Glyph; data.payment shows the refund.
pingSent by the test endpoint.

Subscribe to specific events, or to * for all of them.

Register

POST/api/intake/webhooks
{ "url": "https://shop.example.com/glyph/webhook", "events": ["order.paid", "order.shipped", "order.cancelled"], "description": "Store sync" }

201 returns the webhook and its secret, once:

{
  "id": "686da967-…",
  "url": "https://shop.example.com/glyph/webhook",
  "events": ["order.paid", "order.shipped", "order.cancelled"],
  "active": true,
  "description": "Store sync",
  "created_at": "2026-10-12T21:16:41.555+00:00",
  "secret": "whsec_QLcgi8…",
  "note": "Store the secret now; it is not shown again. Verify X-Glyph-Signature with it."
}

Up to 10 webhooks per key. GET /api/intake/webhooks lists them without secrets; DELETE /api/intake/webhooks/{id} removes one; POST /api/intake/webhooks/{id}/test sends a ping now and reports the HTTP status your server returned:

{ "delivered": true, "delivery": { "id": "2e23…", "attempts": 1, "last_status": 200, "last_error": null, "delivered_at": "2026-10-13T03:41:27.934+00:00", "next_attempt_at": null } }

The same body comes back with 502 and "delivered": false when your server did not answer 2xx.

What we send

POST https://shop.example.com/glyph/webhook
Content-Type: application/json
X-Glyph-Event: order.shipped
X-Glyph-Delivery: 2f0d…
X-Glyph-Signature: t=1760304000,v1=5d41402abc4b2a76b9719d911017c592…
{
  "id": "2f0d…",
  "event": "order.shipped",
  "created_at": "2026-10-14T18:02:12.000Z",
  "data": { "id": "d38f2acc-…", "status": "shipped", "shipment": { "carrier": "USPS", "tracking_number": "9334…" } }
}

Respond with any 2xx within 8 seconds. Do the work afterwards.

Verify the signature

The signature is HMAC-SHA256 over <t>.<raw body> with your secret. Reject it if t is more than five minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, header, rawBody) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
  if (!m) return false;
  const [, t, v1] = m;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(v1, "hex"));
}

Use the raw request body, not a re-serialized copy.

Retries

If your server does not answer 2xx, we retry after 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours, then stop. Deliveries can arrive more than once; use the id to ignore repeats. Events may arrive out of order; trust data.status, not the sequence.