Webhooks

Webhooks let your app react as soon as something happens in a store, such as a new order, a product change or a stock update. Instead of calling the API every few minutes to check, you subscribe to a topic, and EasyStore sends an HTTP POST to your URL each time that event happens.

Common uses: sending new orders to an ERP or accounting system, keeping stock in sync, updating a CRM when customers change, and cleaning up when a merchant uninstalls your app.

Subscribe to a topic

Create a webhook with the Webhooks API, usually right after your app is installed:

curl --request POST 'https://{shop}/api/3.0/webhooks.json' \
--header 'EasyStore-Access-Token: {access_token}' \
--header 'Content-Type: application/json' \
--data '{
    "webhook": {
        "topic": "order/create",
        "url": "https://apps.yourapps.com/webhooks/orders"
    }
}'
  • No extra scope is needed to manage webhooks.
  • Each app has one URL per topic. Subscribing to the same topic again updates its URL.
  • Check the topic spelling. Topics aren't validated: a misspelled topic is saved but never fires.
  • Webhooks are removed when the merchant uninstalls your app. Subscribe again after a reinstall.

Topics

Topic Sent when
App
app/uninstall The merchant uninstalled your app. Sent immediately, before your app's webhooks, script tags and snippets are removed. Body: {"success":true}
Orders
order/create A new order was placed, from any sales channel
order/update An order was changed
order/paid An order was fully paid
order/partially_paid An order received a partial payment
order/cancel An order was cancelled
Fulfillments and refunds
fulfillment/create A fulfillment was created for an order
fulfillment/update A fulfillment changed, for example its tracking or status
fulfillment/cancel A fulfillment was cancelled
refund/create A refund was created
refund/cancel A refund was cancelled
Returns
order_returns/created A return was requested
order_returns/accepted A return was accepted
order_returns/updated A return was updated
order_returns/completed A return was completed
order_returns/cancelled A return was cancelled
Products and inventory
product/create A product was created
product/update A product or its variants changed, including stock changes
product/delete A product was deleted
inventory_level/inventory_quantity_update Stock changed for a variant at a location. Includes variant_id, location_id, inventory_quantity, adjustment (SET, INCREASE or DECREASE) and inventory_level_updated_at
Customers
customer/create A customer was created
customer/update A customer was changed
customer/delete A customer was deleted
Store
store/update Store settings changed
location/create A location was created
location/update A location was changed
location/delete A location was deleted
currency/update A store currency was added or changed
currency/delete A store currency was removed
purchase_order/create A purchase order was created
purchase_order/update A purchase order was changed

Webhooks are only sent while your app is enabled in the store.

Receive a webhook

EasyStore sends an HTTP POST with a JSON body and these headers:

Header Value
Easystore-Topic The topic, for example order/create
Easystore-Shop-Domain The store's domain, for example example.easy.co. Use it to find the store in your database.
Easystore-Hmac-Sha256 The signature to verify
EasyStore-Webhook-Context On product/update and inventory topics only: what caused the change, for example order/create or inventory_level/update
EasyStore-Webhook-Trigger-By-App On product/update and inventory topics only: the handle of the app whose order caused a stock change. Use it to ignore changes your own app caused.

The body is the resource that changed, as JSON. Its shape depends on the topic: order topics send {"order": {...}}, while product topics send the product's fields directly. Webhook payloads can differ slightly from the same resource in the API, so if you need every field, fetch the record with the API using the ID from the webhook.

Respond and retries

  • Respond with 200 OK, ideally within 10 seconds. Any other status, including 201, 204 and redirects, counts as a failure.
  • Do the work later: save the webhook to a queue, respond 200, then process it. See Best practices.
  • Retries: a failed webhook is retried several times, for up to 2 days.
  • Duplicates and order: a webhook can arrive more than once, and webhooks can arrive out of order. Make your handler idempotent, and compare timestamps such as updated_at (or inventory_level_updated_at) before overwriting newer data.
  • Webhooks can be missed. Run a periodic reconciliation with the API as a safety net. See Keeping data in sync.

Verify a webhook

The Easystore-Hmac-Sha256 header is a hex-encoded HMAC-SHA256 of the raw request body, using your app's Client secret as the key. Compute it and compare it with the header; if they match, the webhook came from EasyStore and wasn't changed.

<?php
$secret = getenv('CLIENT_SECRET');
$data = file_get_contents('php://input');
$hmac_header = $_SERVER['HTTP_EASYSTORE_HMAC_SHA256'];

$calculated_hmac = hash_hmac('sha256', $data, $secret);
$verified = hash_equals($calculated_hmac, $hmac_header);

For a test value to check your code, see Webhook HMAC verification.

Test your webhooks

Webhooks are sent from EasyStore's servers, so your URL must be publicly reachable. You can't use:

  • localhost
  • "Fake" domains like www.example.com
  • EasyStore domains (easy.co, easystore.co and www.easystore.co)

Use HTTPS. During development, a tunnelling tool such as ngrok or Cloudflare Tunnel can expose your local server. Then trigger events in your development store, for example by placing a test order.

Also see Limitations and Best practices.

icon-accounticon-add-newicon-add-storeicon-appicon-appleicon-archiveicon-arrowdownicon-ascicon-bookicon-cancelicon-cart-addonicon-checkouticon-cherryicon-collectionicon-comfirmicon-confirmicon-couponicon-creditsicon-currencyicon-dashboardicon-discounticon-disintegrateicon-domainicon-dscicon-duplicateicon-editicon-emailicon-exclamation-triangleicon-exporticon-eyeicon-eye-slashicon-fullscreenicon-fullscreen-closeicon-generalicon-gifticon-gridicon-hddicon-helpicon-importicon-infoicon-integrationicon-invoiceicon-likeicon-listicon-locationicon-logouticon-new-tabicon-not-secureicon-optionicon-ordericon-outline-arrowdownicon-pageicon-paymenticon-plusicon-posicon-pricingicon-printericon-producticon-product-sumicon-product-sum-xicon-redirecticon-reporticon-reseticon-searchicon-secureicon-settingicon-shippingicon-staricon-storeicon-switch-storeicon-tagicon-taxesicon-templateicon-themeicon-tickicon-trashicon-unarchiveicon-uploadicon-user-tagicon-usersicon-weighticon-wholesale