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. Use get_order for reconciliation and on-demand reads.
For background on how orders work, see About orders. For the canonical specification, see the UCP order capability.
Anchor to How it worksHow 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.
Anchor to TopicsTopics
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.
Anchor to Delivery and retriesDelivery 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.
Anchor to HeadersHeaders
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 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. | 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 |
Anchor to HMAC verificationHMAC 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.
To verify a webhook:
- Read the raw request body without parsing.
- Compute
HMAC-SHA256(body, shared_secret)and base64-encode the result. - Compare with the
X-Shopify-Hmac-SHA256header value using a constant-time string comparison. - Reject the request if the signatures don't match.
Verifying the HMAC
Node.js
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
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
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)
endAnchor to Example payloadExample 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 on the Order MCP page.
Order webhook body
Anchor to Best practicesBest 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. - 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.