Skip to content

Webhooks

An event is a fact about the tenant’s product state that an integration may want to react to: a design committed, a print completed, an order’s status changing. Each event is sent as a signed POST to every webhook endpoint subscribed to its type. The body is the same envelope for every type, with the payload described under each event below:

{
  "id": "1790000000000-0192f5a4-7c1e-4d3a-9b6e-2f4c8a1d5e70",
  "type": "design.committed",
  "createdAt": 1790000000000,
  "payload": {
    "designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
    "templateId": "tshirt-basic@2",
    "apiKeyId": "3b2d9c1e-5a4f-4e7d-8c6b-1a0f9e8d7c6b"
  }
}

A webhook endpoint is registered in the studio, or by your server with its secret key (POST /v1/webhooks), each optionally narrowed to a set of event types. Each endpoint has its own signing secret, shown once when it is registered. A secret key lists and manages only the endpoints it registered itself, and they are deleted with it; the studio shows and manages every endpoint.

Verifying a request

Every request is signed with the endpoint’s secret. Verify it before trusting the body:

Header Value
x-pixonaut-timestamp When this attempt was sent, in Unix seconds.
x-pixonaut-signature v1= followed by the hex HMAC-SHA256, keyed with the endpoint’s signing secret, of the x-pixonaut-timestamp value, a ., and the raw request body.

Refuse a request whose x-pixonaut-timestamp is more than 300 seconds from your own clock. A later signature scheme would be sent in the same header beside v1=, comma-separated, so check the v1= value and ignore any other.

Compare the signature in constant time, over the raw body bytes as they arrived, before parsing them: re-serialising parsed JSON changes the bytes. The timestamp is the moment that attempt was sent, not when the event happened, so a retry of an old event still verifies, and a copy of an earlier attempt replayed later does not.

To test a verifier, webhook-signature-vectors.json, published with every API release beside this reference (/api/<version>/ on the CDN), holds requests signed by the same code that signs every delivery: ones a correct verifier accepts, and ones it refuses, among them a request sent longer ago than the tolerance and a signature header without its scheme. Run your verifier with its clock at the file’s now (Unix seconds).

Acknowledging, retries and redelivery

Answer any 2xx to acknowledge a request. Anything else, a redirect, or no answer in time is a failed attempt. A delivery (one event sent to one endpoint) is retried until the endpoint accepts it or the retries run out, at which point it has failed and stays failed until someone redelivers it.

A delivery is tried at most 22 times. The gap before each retry grows, from 30 seconds after the first failed attempt to 3.7 hours before the last, so the retries run out about 27.6 hours after the first attempt.

To redeliver is to send a delivery again from the start, with a fresh set of retries: a failed one, or a delivered one the receiver lost. It is done from the endpoint’s delivery log in the studio, or by the secret key that registered it (a connected store’s endpoint, by the store’s integration key): one delivery (POST /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver), or every failed one at once after downtime (POST /v1/webhooks/{id}/deliveries/redeliver-failed). Either can also send a test request (a ping) on demand. A ping is not an event: it is signed the same way but never retried, and its type is not one an endpoint can subscribe to.

A retry or a redelivery carries the same event id, so drop a request whose id you have already handled. Deliveries can arrive out of order; use createdAt to order events about the same thing.

Missed deliveries

Your server can also pull the events it may have missed, in the order they were recorded, with GET /v1/events?after=<event id>: give it the id of the last event a previous pull answered, leaving after out the first time, and narrow it with types (comma-separated) to the types your endpoint subscribes to. Keep that checkpoint apart from the deliveries you receive: deliveries can arrive out of order, so the last one handled can be later than one still being retried. Each event comes in the same envelope a delivery’s body carries. Follow nextCursor as the next after until it is null, when you have caught up.

Rules a receiver can rely on

  • An endpoint registered with an empty set of event types receives every type. New types are added over time, and a subscription to every type receives them as they appear, so a receiver ignores types it does not know.
  • An update event names the fields the change set, without claiming their values differ from before.

Questions? Write to [email protected].

Status · Terms of service · Privacy policy · Subprocessors