Skip to content

Best Practices

Webhook delivery is at least once and unordered. A receiver that assumes each event arrives exactly once, in order, works in testing and breaks in production. These habits keep it correct.

Check OpenShop-Signature against the raw body first, and only then parse the JSON. Anything that fails verification is not from Open Shop; answer 400 and do nothing else. Verify Signatures has copy-paste verifiers.

Open Shop waits 10 seconds for a 2xx. Anything slower is a failure and is retried, even if your code finished the work afterwards, so the next attempt does it again.

Verify, store the event or push it onto a queue, return 200, and do the real work in a background job. Calls to other services, emails, and database writes that fan out all belong in that job, not in the request.

Retries, resends, and a lost response can all deliver the same event more than once. The id (also sent as OpenShop-Event-Id) is identical on every copy.

Record each id you have processed, for example with a unique constraint in a table of handled events, and skip any you have already seen. Keep the record for at least 30 days: that covers the retry window of about 3 days and the delivery log’s 30-day retention, during which an admin can resend any delivery.

Deliveries are sent in parallel and retried independently, so shipment.status_changed to DELIVERED can arrive before the earlier change to IN_TRANSIT, and a retried event can land hours after newer ones.

Before applying an event, compare its createdOn with the createdOn of the last event you applied for the same object, and skip it if it is older. Customer and refund snapshots also carry their own modifiedOn; for those, compare that as well. Order, shipment, and subscription snapshots have no modifiedOn, so rely on the event’s createdOn. An older event should never overwrite newer state.

Do the comparison and the write as one atomic step, because two deliveries for the same object can be processed at the same time. A conditional update such as UPDATE ... SET state = $2, last_event_on = $1 WHERE id = $3 AND (last_event_on IS NULL OR last_event_on < $1), or a transaction that locks the row first, stops an older event from winning the race. The IS NULL branch accepts the first event for an object. Two different events about one object with the same createdOn cannot be ordered; this example keeps whichever was applied first, which is safe because neither is newer. If you need every change rather than the latest state, fetch the object from the API instead of relying on the tie.

Treat the snapshot as the state at event time

Section titled “Treat the snapshot as the state at event time”

data.object is the object as it was when the event happened, frozen into the delivery. A retry two days later still carries that original snapshot, not the object as it is now.

That makes the snapshot right for “what happened” and wrong for “what is true now”. When you need the latest state, for example before shipping goods or granting access, re-fetch the object through the public API and act on that.

Pick the specific event types your integration handles rather than Select all events. Fewer deliveries means less load on your server and a delivery log that only shows what matters. An endpoint subscribed to all events also receives types added in later releases, so a catch-all receiver must ignore types it does not recognise rather than failing on them.

The signing secret is a credential. Load it from an environment variable or your secrets manager, never commit it, and keep it out of logs. If it leaks, Rotate it from the endpoint’s detail view; the old secret stops verifying straight away.

A 410 Gone response disables the endpoint immediately, without retries. Use it when a URL is retired and should never receive events again. For anything temporary, such as a deploy, a bad signature, or a dependency being down, return a 5xx or 400 and let the retries carry you through.

The admin’s webhook delivery log shows every delivery, each attempt’s status code and response, and when the next retry is due. Admins who opt in get an email the first time a delivery fails all of its attempts and again if the endpoint is auto-disabled. Act on the first email: after 3 days of continuous failure the endpoint is disabled, and events during the disabled window are not replayed.