Receive webhooks
A quick start for the server that receives Pixonaut’s events: register an endpoint, verify each request, and acknowledge it. The rules behind each step, the retry schedule and every event’s payload are under Webhooks.
1. Register an endpoint
In the studio, under Webhooks, choose Add endpoint and give it the URL your server receives on. Leave Event types unchecked to receive every event, or check the ones you handle. Copy the endpoint’s signing secret when it is shown: it is shown once.
2. Verify each request
Read the raw body as it arrived, before parsing it, and check its signature with the endpoint’s secret. In Node:
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 300;
const MS_PER_SECOND = 1000;
// `headers` with lowercase names, `rawBody` the bytes as they arrived.
export function verifyWebhook(secret, headers, rawBody, now = Date.now()) {
const timestamp = Number(headers["x-pixonaut-timestamp"]);
if (!Number.isInteger(timestamp)) return false;
if (Math.abs(now / MS_PER_SECOND - timestamp) > TOLERANCE_SECONDS) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(String(timestamp))
.update(".")
.update(rawBody)
.digest();
// The header may carry other schemes beside this one, comma-separated.
return String(headers["x-pixonaut-signature"] ?? "")
.split(",")
.some((entry) => {
const [scheme, hex = ""] = entry.trim().split("=");
if (scheme !== "v1" || !/^[0-9a-f]+$/.test(hex)) return false;
const given = Buffer.from(hex, "hex");
return (
given.length === expected.length &&
crypto.timingSafeEqual(given, expected)
);
});
}
Refuse a request that does not verify, without acting on its body.
3. Store, then acknowledge
A delivery answered with a 2xx is never sent again, so answer one only
once the event is safe: either handle it before answering, or store or
enqueue the verified event durably and answer 200 then, handling it from
there. A slow or failed answer is retried, and a retry carries the same
event id, so skip an id whose work has succeeded. Count an id as handled
only once its work has, so that work which failed is done again when the
event is retried or picked up from your queue:
{
"id": "1790000090000-3b4c5d6e-7f8a-4b9c-8d0e-1f2a3b4c5d6e",
"type": "print.completed",
"createdAt": 1790000090000,
"payload": { "printId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f" }
}
4. Follow what a store sells
A template a store sells can be paused by a member, after which customers
cannot design on it. Each pause and each resume is one template.live_changed,
naming the template by baseId: live: false is a pause, live: true a
resume. Hide the designer for a paused template and show it again once it is
resumed. A delivery can be missed, and a store connected after a pause heard
nothing of it, so the event is a prompt rather than the record: status on
GET /v1/templates/{id} answers live or paused for any version of the
template, and is the state to trust.
A store that keeps the address of a link it handed out, such as a designer
link on a product page, follows share_link.revoked and
share_link.regenerated: the address it kept no longer opens. Neither
carries the new address; a designer link’s is read again from the share-link
list, as its url. A connected store’s endpoint hears of the links its own
keys handed out; an endpoint no store owns hears of every one.
Use Send test request in the endpoint’s delivery log to check that your server is reachable and verifies; the test request is signed the same way.
Questions? Write to [email protected].