Webhooks
Events Poltura sends to your server, with the topics, their data, the Ed25519 signature, how to verify it, and the retry schedule.
On this page
A webhook is a signed POST from Poltura to your server when something happens in a store that installed your
app: an order is placed, a product changes, the app is updated or removed. Subscribe to topics in webhooks in
the manifest, up to 16, each with its own HTTPS destination.
Delivery is at least once: the same event can arrive twice, so keep the eventId of what you processed and
skip repeats. The body is the event's stored JSON, byte for byte; verify it before you parse it, and refuse
bodies over 256 KiB.
Topics
A topic is delivered only while the installation holds the permission next to it. The app.* lifecycle topics need none:
| Topic | Permission | data |
|---|---|---|
product.created | products.read | ProductEventData { productId, handle, title, status, variants? } (variants max 100) |
product.updated | products.read | same |
product.deleted | products.read | { productId } |
order.created | orders.read | OrderEventData { orderId, orderNumber, status, paymentStatus, fulfillmentStatus, currency, total, customerId } |
order.updated | orders.read | same |
order.paid | orders.read | same |
order.fulfilled | orders.read | same |
order.cancelled | orders.read | same |
customer.created | customers.read | { customerId, email } |
customer.updated | customers.read | same |
inventory.updated | products.read | { inventoryItemId, variantId, locationId, available } |
app.installed | none | InstallationLifecycleEventData |
app.updated | none | same |
app.suspended | none | same |
app.uninstalled | none | same |
Every event has the same envelope: { eventId, topic, apiVersion: 'v1', occurredAt, storeId, installationId, resourceVersion, data }. A product's status is draft, published or archived. A product event without variants carries no variant list; [] means the product has none. app.installed arrives when the installation is ready, not while it is being prepared.
Signature
Poltura signs <unix seconds>.<raw body> with its Ed25519 key, the same key that signs actions. The signature is 64 bytes, sent as version v1. The headers:
| Header | Meaning |
|---|---|
poltura-timestamp | Unix seconds when Poltura signed it. Requests more than 300 seconds off are refused |
poltura-key-id | Which platform key signed it: letters, digits, ., _ and -, at most 64 |
poltura-signature | v1= and the signature, base64url without padding |
poltura-event-id | When present, must equal the body's eventId |
poltura-topic | When present, must equal the body's topic |
poltura-delivery-id | This delivery attempt's id. Not signed; for your logs only |
poltura-attempt | The attempt number. Not signed; for your logs only |
Create one verifier with createWebhookVerifier from @poltura/app-sdk/webhooks. It takes 1 to 8 platform
public keys (Ed25519, PEM), picks the one named by poltura-key-id, and, if you pass both requiredStoreId and
requiredInstallationId, refuses events for any other installation. The production key is not published yet
(see Authentication).
const verifierOptions: WebhookVerifierOptions = {
keys: [{ kid: env.POLTURA_WEBHOOK_KID, publicKeyPem: env.POLTURA_WEBHOOK_PUBLIC_KEY }],
};
if (env.POLTURA_INSTALLATION_ID !== undefined) {
verifierOptions.requiredStoreId = env.POLTURA_STORE_ID;
verifierOptions.requiredInstallationId = env.POLTURA_INSTALLATION_ID;
}
const verifier = createWebhookVerifier(verifierOptions);Then verify the raw body of each request before you use it:
const raw = await readBody(req);
const event = verifier.verify(raw, requestHeaders(req));
if (event.topic === PublicTopics.ORDER_CREATED) {
await saveLastOrder(client(), event.data.orderId, 0, event.eventId);
}
send(res, 200, event.topic);const raw = await readBody(req);
const event = verifier.verify(raw, requestHeaders(req));
send(res, 200, event.topic);verify returns the typed event. A retry carries the same eventId with a new delivery id, so store each
eventId you have handled and skip it the second time. A failed check throws PolturaWebhookError with a code:
invalid, wrong_algorithm, unknown_kid, expired_timestamp, future_timestamp, noncanonical_signature,
payload_too_large, altered_body or tenant_mismatch. Answer those with a 4xx.
Retries
Answer with a 2xx within 10 seconds to acknowledge an event. Otherwise Poltura tries again, up to 8 times in
all, waiting longer each time, from 1 second up to 5 minutes. After the last attempt the delivery is given up.
Only the first 64 KiB of your answer is read.
app.suspended and app.uninstalled go to the destinations of the release the store had approved, even after
your app has been removed. If that release had no destination for them, they are not sent.
Next
- Authentication: the same signature on actions.
- Permissions and topics: every topic, generated from the platform.