Developers
Start building
Apps/Webhooks

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:

TopicPermissiondata
product.createdproducts.readProductEventData { productId, handle, title, status, variants? } (variants max 100)
product.updatedproducts.readsame
product.deletedproducts.read{ productId }
order.createdorders.readOrderEventData { orderId, orderNumber, status, paymentStatus, fulfillmentStatus, currency, total, customerId }
order.updatedorders.readsame
order.paidorders.readsame
order.fulfilledorders.readsame
order.cancelledorders.readsame
customer.createdcustomers.read{ customerId, email }
customer.updatedcustomers.readsame
inventory.updatedproducts.read{ inventoryItemId, variantId, locationId, available }
app.installednoneInstallationLifecycleEventData
app.updatednonesame
app.suspendednonesame
app.uninstallednonesame

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:

HeaderMeaning
poltura-timestampUnix seconds when Poltura signed it. Requests more than 300 seconds off are refused
poltura-key-idWhich platform key signed it: letters, digits, ., _ and -, at most 64
poltura-signaturev1= and the signature, base64url without padding
poltura-event-idWhen present, must equal the body's eventId
poltura-topicWhen present, must equal the body's topic
poltura-delivery-idThis delivery attempt's id. Not signed; for your logs only
poltura-attemptThe 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

Last updated