Skip to content

Webhooks

Webhooks let Open Shop tell your server when something happens in your store, instead of your server polling for it. When an order is placed, a shipment picks up tracking, or a subscription renews, Open Shop sends a signed JSON POST to every endpoint you registered for that event type.

Each event carries a snapshot of the object it is about, in the same shape the public API returns, plus a summary of what changed. V1 covers five families: orders, refunds, shipments, subscriptions, and customers. The full list is on Event Types.

Endpoints are managed by a store admin in the Open Shop admin. There is no public API for them.

  1. Open Settings → Integrations → Webhooks and choose Add endpoint.

  2. Enter the endpoint URL. It must be https, publicly reachable, and must not carry credentials in the URL.

  3. Pick the event types to receive, or Select all events. An endpoint subscribed to order.created receives order.created and nothing else. An endpoint subscribed to all events also picks up types added in later releases.

  4. Save, then copy the signing secret from the endpoint’s detail view. It starts with whsec_ and is what your server uses to verify signatures. Revealing it again later is recorded in the store’s Activity log.

  5. Use Send test event on the same view. It delivers a sample event once, with livemode: false, and shows the status code, latency, and response body your server returned. Test deliveries never count toward endpoint health.

Every delivery is an HTTP POST with a JSON body and these headers:

HeaderValue
Content-Typeapplication/json
User-AgentOpenShop-Webhooks/1.0
OpenShop-Event-IdThe event id. The same on every attempt.
OpenShop-Event-TypeThe event type, for example order.fulfillment_status_changed.
OpenShop-Delivery-AttemptThe attempt number, starting at 1.
OpenShop-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>. See Verify Signatures.

The body is one event:

order.fulfillment_status_changed
{
"id": "evt_01JA3K8Q2V7XN4M9RZB6TC5HFE",
"type": "order.fulfillment_status_changed",
"apiVersion": "2026-10-01",
"createdOn": "2026-10-08T17:42:11Z",
"storeId": 5,
"livemode": true,
"data": {
"object": { "orderId": 48211, "orderNumber": 100482, "...": "the full order snapshot" },
"changes": { "fulfillmentStatus": { "old": "UNFULFILLED", "new": "FULFILLED" } },
"previousAttributes": null
},
"source": { "action": "order.fulfillment_status_changed", "actorType": "ADMIN" }
}
FieldWhat it is
idUnique event id, evt_ followed by a ULID. Stable across retries and shared by every endpoint that receives the same event.
typeThe event type.
apiVersionThe payload version, pinned to the endpoint when it was created.
createdOnWhen the event happened, ISO 8601 UTC.
storeIdThe store the event belongs to.
livemodefalse for test events sent from the admin, true otherwise.
data.objectSnapshot of the object at the moment of the event. The shape per family is on Event Types.
data.changesOld and new values for each field that changed, keyed by field name, or null when the event carries no field changes.
data.previousAttributesReserved. Always null in V1.
source.actionThe internal action that produced the event. Several actions can map to one event type; this tells you which one fired.
source.actorTypeWho caused it: ADMIN, CUSTOMER, API_TOKEN, SYSTEM, or PLATFORM.
  • At least once. A delivery can arrive more than once, for example when your server processed it but the response was lost. Deduplicate on id.
  • Unordered. Deliveries for one endpoint are sent in parallel and retried independently, so a later event can arrive before an earlier one. Best Practices covers how to cope.
  • Success is any 2xx within 10 seconds. The response body is ignored, apart from the first 4 KB being kept for the delivery log.
  • Everything else is a failure and is retried: timeouts, connection and TLS errors, 4xx, and 5xx. The one exception is 410 Gone, below.
  • Redirects are not followed. A 3xx is a failure. Register the final URL.
  • 410 Gone disables the endpoint immediately and is not retried. Return it when you want Open Shop to stop sending to that URL for good.

A failed delivery is retried with backoff, for 11 attempts over about 3 days. Every attempt carries the same event id and payload, with a fresh signature timestamp.

AttemptDelay after previousElapsed since first attempt
1immediate0
21 min1 min
35 min6 min
430 min36 min
52 h2 h 36 min
65 h7 h 36 min
710 h17 h 36 min
812 h1 d 5 h
912 h1 d 17 h
1012 h2 d 5 h
1112 h2 d 17 h

A delivery that fails all 11 attempts is marked Failed.

Health is measured on failed deliveries, not on individual attempts, so a short outage that recovers within the retry window never counts against you.

  • Any 2xx on any delivery resets the endpoint’s failure streak.
  • When a delivery first fails all of its attempts, admins who opted in to webhook notifications get an email. Further failures in the same streak send at most one email per 24 hours.
  • If deliveries keep failing for 3 consecutive days with no success in between, the endpoint moves to Auto-disabled, its pending retries are cancelled, and the same admins get a second email.
  • A 410 Gone response disables the endpoint straight away, with the same email.

To bring an endpoint back, fix the receiver and choose Re-enable on the endpoint’s detail view. That resets its health counters.

Every delivery is recorded in the admin’s webhook delivery log: the exact payload sent, each attempt with its status code, duration, error and truncated response body, and when the next retry is due. The log can be filtered by endpoint, event type, status, or the order, refund, subscription, shipment, customer, address or payment method the event was about.

From a delivery’s detail view an admin can Resend any delivery that is not still pending. A resend uses the stored payload and the same event id, with a fresh signature and a new set of attempts.

Deliveries are kept for 30 days.

Endpoints must be public https URLs, so a server on your own machine needs a tunnel. Start your receiver locally, for example one of the samples in Verify Signatures, then expose its port with cloudflared or ngrok:

Terminal window
cloudflared tunnel --url http://localhost:4010
# or
ngrok http 4010

Register the https URL the tunnel prints as an endpoint, then use Send test event to see a delivery land. To watch retries and auto-disable play out before going live, have the receiver answer 500, wait longer than 10 seconds, or return 410 Gone on purpose, and follow the attempts in the delivery log.