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.
Register an endpoint
Section titled “Register an endpoint”Endpoints are managed by a store admin in the Open Shop admin. There is no public API for them.
-
Open Settings → Integrations → Webhooks and choose Add endpoint.
-
Enter the endpoint URL. It must be
https, publicly reachable, and must not carry credentials in the URL. -
Pick the event types to receive, or Select all events. An endpoint subscribed to
order.createdreceivesorder.createdand nothing else. An endpoint subscribed to all events also picks up types added in later releases. -
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. -
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.
The request
Section titled “The request”Every delivery is an HTTP POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | OpenShop-Webhooks/1.0 |
OpenShop-Event-Id | The event id. The same on every attempt. |
OpenShop-Event-Type | The event type, for example order.fulfillment_status_changed. |
OpenShop-Delivery-Attempt | The attempt number, starting at 1. |
OpenShop-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>. See Verify Signatures. |
The body is one event:
{ "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" }}| Field | What it is |
|---|---|
id | Unique event id, evt_ followed by a ULID. Stable across retries and shared by every endpoint that receives the same event. |
type | The event type. |
apiVersion | The payload version, pinned to the endpoint when it was created. |
createdOn | When the event happened, ISO 8601 UTC. |
storeId | The store the event belongs to. |
livemode | false for test events sent from the admin, true otherwise. |
data.object | Snapshot of the object at the moment of the event. The shape per family is on Event Types. |
data.changes | Old and new values for each field that changed, keyed by field name, or null when the event carries no field changes. |
data.previousAttributes | Reserved. Always null in V1. |
source.action | The internal action that produced the event. Several actions can map to one event type; this tells you which one fired. |
source.actorType | Who caused it: ADMIN, CUSTOMER, API_TOKEN, SYSTEM, or PLATFORM. |
Delivery guarantees
Section titled “Delivery guarantees”- 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
2xxwithin 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, and5xx. The one exception is410 Gone, below. - Redirects are not followed. A
3xxis a failure. Register the final URL. 410 Gonedisables the endpoint immediately and is not retried. Return it when you want Open Shop to stop sending to that URL for good.
Retry schedule
Section titled “Retry schedule”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.
| Attempt | Delay after previous | Elapsed since first attempt |
|---|---|---|
| 1 | immediate | 0 |
| 2 | 1 min | 1 min |
| 3 | 5 min | 6 min |
| 4 | 30 min | 36 min |
| 5 | 2 h | 2 h 36 min |
| 6 | 5 h | 7 h 36 min |
| 7 | 10 h | 17 h 36 min |
| 8 | 12 h | 1 d 5 h |
| 9 | 12 h | 1 d 17 h |
| 10 | 12 h | 2 d 5 h |
| 11 | 12 h | 2 d 17 h |
A delivery that fails all 11 attempts is marked Failed.
Endpoint health and auto-disable
Section titled “Endpoint health and auto-disable”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
2xxon 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 Goneresponse 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.
Delivery log
Section titled “Delivery log”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.
Test locally
Section titled “Test locally”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:
cloudflared tunnel --url http://localhost:4010# orngrok http 4010Register 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.