--- title: Order webhooks description: >- Receive UCP-shaped order lifecycle events at your endpoint as orders are created, updated, and deleted on Shopify. source_url: html: 'https://shopify.dev/docs/agents/orders/order-webhooks' md: 'https://shopify.dev/docs/agents/orders/order-webhooks.md' --- # Order webhooks Shopify pushes UCP-shaped order webhooks to your registered endpoint whenever an order placed through your agent has a committed change (fulfillment progress, refunds, returns, exchanges, order edits, or cancellations). Webhooks are the primary update channel for the [order capability](https://shopify.dev/docs/agents/orders). Use [`get_order`](https://shopify.dev/docs/agents/orders/order-mcp) for reconciliation and on-demand reads. For background on how orders work, see [About orders](https://shopify.dev/docs/agents/orders). For the canonical specification, see the [UCP order capability](https://ucp.dev/2026-04-08/specification/order/). *** ## How it works Each delivery contains the full current state of the order. The payload is identical to what `get_order` returns at that moment. Don't reconstruct order state by replaying webhook events. Treat the latest payload as the source of truth. Order webhook delivery URL and topic are configured by Shopify. There's no self-serve subscription API today: your delivery URL and topic scoping are registered server-side. To set up or update an order webhook subscription, contact your Shopify partner manager. The canonical UCP specification lets agents advertise a `webhook_url` in their UCP profile so platforms can subscribe automatically, but Shopify doesn't honor that field today. ### Topics Every order webhook delivery has the same UCP-shaped payload, so route all order webhooks through a single handler and inspect the data to determine what changed. Don't branch on the topic name. For reference, deliveries fire on three underlying topics: | Topic | When it fires | | - | - | | `orders/create` | A new order is placed through your agent. | | `orders/updated` | The order changes (payment captured, fulfillment progresses, an adjustment commits, or other internal updates). | | `orders/delete` | An order is deleted. The payload still contains the order's last-known state. | Some `orders/updated` deliveries might not change UCP-relevant fields (the underlying trigger updated something internal). Expect the occasional duplicate-looking payload, and use the `X-Shopify-Webhook-Id` header to deduplicate retries of the same event. ### Delivery and retries Order webhooks are delivered over HTTPS as `POST` requests with a JSON body. Shopify follows standard webhook retry behavior. Failed deliveries are retried up to 8 times over 4 hours with exponential backoff. Respond with a 2xx status code as quickly as possible to acknowledge receipt. Long-running work should happen out of band. For more on monitoring delivery failures and best practices, see [Webhook best practices](https://shopify.dev/docs/apps/build/webhooks/best-practices). *** ## Headers Order webhook requests include the following headers. | Header | Description | Example | | - | - | - | | `X-Shopify-Topic` | The webhook topic. | `orders/updated` | | `X-Shopify-Shop-Domain` | The merchant's `myshopify.com` domain. | `cool-store.myshopify.com` | | `X-Shopify-API-Version` | API version of the webhook subscription. | `2026-04` | | `X-Shopify-Triggered-At` | [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339) timestamp with nanosecond precision. | `2026-03-30T14:30:00.000000000Z` | | `X-Shopify-Webhook-Id` | A unique composite key per delivery. Use to [identify and deduplicate individual deliveries](https://shopify.dev/docs/apps/build/webhooks/ignore-duplicates). | `b54557e4-bdd9-4b37-8a5f-bf7d70bcd043` | | `X-Shopify-Hmac-SHA256` | Base64-encoded HMAC-SHA256 of the raw request body. Use this header to verify the signature. | `XWmrwMey6OsLMeiZKwP4FppHH3cmAiiJJAweH5Jo4bM=` | | `X-Shopify-Event-Id` | A unique ID shared across all deliveries produced by the same merchant action. | `7b8c9d0a-1234-5678-90ab-cdef12345678` | | `Webhook-Id` | Standard Webhooks delivery ID. Same value as `X-Shopify-Webhook-Id`. | `b54557e4-bdd9-4b37-8a5f-bf7d70bcd043` | | `Webhook-Timestamp` | Standard Webhooks delivery timestamp, as Unix epoch seconds (string). | `1745594400` | *** ## HMAC verification The HMAC is computed as `base64(HMAC-SHA256(raw_body, shared_secret))` using your global app's shared secret. This is the same `client_secret` value you use when minting Order MCP access tokens, retrieved from the **Catalog** section of [Dev Dashboard](https://shopify.dev/docs/apps/build/dev-dashboard). To verify a webhook: 1. Read the raw request body without parsing. 2. Compute `HMAC-SHA256(body, shared_secret)` and base64-encode the result. 3. Compare with the `X-Shopify-Hmac-SHA256` header value using a constant-time string comparison. 4. Reject the request if the signatures don't match. ## Verifying the HMAC ##### Node.js ```javascript import crypto from "node:crypto"; function verifyOrderWebhook(rawBody, headers, sharedSecret) { const provided = headers["x-shopify-hmac-sha256"]; if (!provided) return false; const computed = crypto .createHmac("sha256", sharedSecret) .update(rawBody) .digest("base64"); const providedBuf = Buffer.from(provided, "utf8"); const computedBuf = Buffer.from(computed, "utf8"); if (providedBuf.length !== computedBuf.length) return false; return crypto.timingSafeEqual(providedBuf, computedBuf); } ``` ##### Python ```python import base64 import hashlib import hmac def verify_order_webhook(raw_body: bytes, headers: dict, shared_secret: str) -> bool: provided = headers.get("x-shopify-hmac-sha256", "") computed = base64.b64encode( hmac.new( shared_secret.encode("utf-8"), raw_body, hashlib.sha256, ).digest() ).decode("utf-8") return hmac.compare_digest(provided, computed) ``` ##### Ruby ```ruby require "base64" require "openssl" def verify_order_webhook(raw_body, headers, shared_secret) provided = headers["X-Shopify-Hmac-SHA256"].to_s computed = Base64.strict_encode64( OpenSSL::HMAC.digest("sha256", shared_secret, raw_body) ) return false if provided.bytesize != computed.bytesize OpenSSL.fixed_length_secure_compare(provided, computed) end ``` *** ## Example payload Every order webhook delivery is the full current state of the order, wrapped in a UCP envelope. The body is identical in shape to what `get_order` returns. For the field-level reference and an example payload, see the [Order data model](https://shopify.dev/docs/agents/orders/order-mcp#order-data-model) on the Order MCP page. ## Order webhook body ```json { "ucp": { "version": "2026-04-08", "capabilities": { "dev.ucp.shopping.order": [{ "version": "2026-04-08" }] } }, "id": "gid://shopify/Order/6252199051413", "label": "#1042", "checkout_id": "gid://shopify/Checkout/7904f172582fa0de7a67f1839f8ed7ae?key=3d5fe2f0d03a5ecf432a17c6c4c1dde5", "permalink_url": "https://cool-store.myshopify.com/96686207971/orders/eaadd7a71f2656ef2684275972df05da/authenticate", "currency": "USD", "totals": [ { "type": "subtotal", "display_text": "subtotal", "amount": 6298 }, { "type": "fulfillment", "display_text": "fulfillment", "amount": 899 }, { "type": "tax", "display_text": "tax", "amount": 780 }, { "type": "fee", "display_text": "fee", "amount": 0 }, { "type": "total", "display_text": "total", "amount": 7977 } ], "line_items": [ { "id": "gid://shopify/LineItem/14584740544661", "item": { "id": "gid://shopify/ProductVariant/46040179376277", "title": "Large / Black", "price": 3999, "image_url": "https://cdn.shopify.com/s/files/1/2637/1970/products/classic-tee.jpg" }, "quantity": { "original": 1, "total": 1, "fulfilled": 1 }, "totals": [ { "type": "subtotal", "display_text": "subtotal", "amount": 3999 }, { "type": "tax", "display_text": "tax", "amount": 520 }, { "type": "total", "display_text": "total", "amount": 4519 } ], "status": "fulfilled", "parent_id": null }, { "id": "gid://shopify/LineItem/14584740544662", "item": { "id": "gid://shopify/ProductVariant/46040179376290", "title": "One Size", "price": 2299, "image_url": "https://cdn.shopify.com/s/files/1/2637/1970/products/beanie.jpg" }, "quantity": { "original": 1, "total": 1, "fulfilled": 0 }, "totals": [ { "type": "subtotal", "display_text": "subtotal", "amount": 2299 }, { "type": "tax", "display_text": "tax", "amount": 260 }, { "type": "total", "display_text": "total", "amount": 2559 } ], "status": "processing", "parent_id": null } ], "fulfillment": { "expectations": [ { "id": "gid://shopify/Expectation/6252199051413", "line_items": [ { "id": "gid://shopify/LineItem/14584740544661", "quantity": 1 }, { "id": "gid://shopify/LineItem/14584740544662", "quantity": 1 } ], "method_type": "shipping", "destination": { "first_name": "Jane", "last_name": "Smith", "street_address": "123 Main Street", "address_locality": "Brooklyn", "address_region": "NY", "address_country": "US", "postal_code": "11201" } } ], "events": [ { "id": "gid://shopify/Fulfillment/5650582175893?status=created", "occurred_at": "2026-03-25T14:30:00-04:00", "type": "shipped", "description": null, "tracking_number": "1Z999AA10123456784", "tracking_url": "https://www.ups.com/WebTracking?trackNums=1Z999AA10123456784", "carrier": "UPS", "line_items": [{ "id": "gid://shopify/LineItem/14584740544661", "quantity": 1 }] }, { "id": "gid://shopify/FulfillmentEvent/8901234567", "occurred_at": "2026-03-26T09:15:00-04:00", "type": "in_transit", "description": "Package is in transit", "tracking_number": "1Z999AA10123456784", "tracking_url": "https://www.ups.com/WebTracking?trackNums=1Z999AA10123456784", "carrier": "UPS", "line_items": [{ "id": "gid://shopify/LineItem/14584740544661", "quantity": 1 }] } ] }, "adjustments": [ { "id": "gid://shopify/SalesAgreement/1234567890", "type": "refund", "occurred_at": "2026-03-27T11:00:00-04:00", "status": "completed", "totals": [{ "type": "total", "amount": -3999 }], "description": null, "line_items": [{ "id": "gid://shopify/LineItem/14584740544661", "quantity": -1 }] } ] } ``` *** ## Best practices * **Treat the latest delivery as truth.** Every payload is the full current state. Don't merge or replay events from previous deliveries. * **Deduplicate on `X-Shopify-Webhook-Id`.** This unique identifying header allows you to track received delivery IDs and skip duplicates. See [Ignore duplicate webhooks](https://shopify.dev/docs/apps/build/webhooks/ignore-duplicates). * **Respond fast.** Acknowledge receipt with a 2xx status as quickly as possible (well under your endpoint's timeout) and process the payload asynchronously. * **Always link out for the canonical experience.** The merchant's order status page (`permalink_url`) is the source of truth for full timeline details, returns initiation, and other post-purchase operations. Surface it in your buyer UI. * **Treat order data as ephemeral.** Per the UCP specification, platforms should discard order data after it's no longer needed for active commerce flows. ***