Skip to main content
Back to blog

Shopify Conversational Commerce — Real-Time Cart Recovery via HMAC-Verified Webhooks

How Devotel Orbit's Shopify integration turns customers, checkouts, orders, and refunds into real-time messaging signals — an HMAC-verified webhook receiver with an hourly poll safety net, GDPR topics on the same endpoint, and a worked abandoned-cart WhatsApp recovery flow.

Orbit Editorial Team

E-commerce messaging usually runs one of two ways. The batch way: export abandoned checkouts to a CSV overnight, upload a segment, send the next morning. The triggered way: the store emits an event the moment a buyer stalls, and the message is a function of that event, not of a campaign calendar. Only the second one catches a buyer while the cart is still open in another tab. Devotel Orbit's Shopify integration wires a store's customers, checkouts, orders, and refunds into Orbit as first-class events over an HMAC-verified webhook receiver, with an hourly poll behind it. This post covers why the triggered model wins, what the integration subscribes to, a worked abandoned-cart WhatsApp recovery flow, the GDPR topics Shopify mandates on the same endpoint, and the numbers that tell you the flow is working.

Why commerce-triggered messaging beats batch campaigns

The difference is structural, not stylistic. Three properties separate an event-driven recovery flow from a nightly blast:

  • Freshness. A checkout event reaches Orbit seconds after the buyer enters checkout and stalls. A batch export is hours old by the time it sends, and hours is where cart intent decays.
  • Self-cancellation. A triggered flow can watch for the buyer's own orders/create and exit the recovery branch the moment the cart closes on its own. A batch campaign has already spent its send on people who completed the purchase hours earlier.
  • Identity continuity. The webhook upserts an Orbit contact keyed on the Shopify customer or checkout id, so the recovery message, the later order, and the post-purchase survey all land on one contact row. CSV imports deduplicate on address strings; event streams merge on provider ids.

Batch still has its place — a seasonal promo to a segment is not an event. Recovery, replenishment, and order lifecycle messaging are events, and they underperform when forced through the campaign calendar.

What the integration subscribes to

On OAuth connect (owner/admin only), Orbit auto-registers the store for the canonical operational topic set, and re-connects are idempotent:

TopicWhat arrives in Orbit
customers/createContact upsert for the new shop customer
checkouts/createContact upsert plus the normalized cart.created signal — the abandonment seed
orders/create, orders/paid, orders/fulfilledOrder event records (placed, paid, shipped)
refunds/createOrder event record (refunded)
app/uninstalledConnection marked inactive when the shop disconnects

Every inbound event hits one endpoint — POST /api/v1/integrations/webhooks/shopify — with four properties worth knowing before you build on it:

Verified, fail-closed. Shopify signs each delivery with a base64 HMAC-SHA256 of the raw body in X-Shopify-Hmac-Sha256, keyed with the app shared secret. The comparison is constant-time: a wrong signature gets 401, an unset secret gets 503. The receiver never silently accepts unsigned traffic.

Tenant-resolved by header. The HMAC is the sole auth; the receiver resolves which tenant owns the event from the X-Shopify-Shop-Domain header, probing a shop-domain reverse map with a stamped fallback so repeat webhooks resolve in O(1).

Idempotent. Deliveries carry a unique X-Shopify-Webhook-Id. The receiver persists each event keyed (shop_domain, webhook_id) and Shopify's retries arrive as no-ops, so exactly-once processing survives Shopify's at-least-once delivery.

Fast. The receiver returns inside Shopify's 5-second budget by enqueuing heavy downstream work instead of awaiting it on the request path.

Behind the real-time path, an hourly poll refreshes customers, orders, products, and abandoned-checkouts. If a delivery is missed or a registration partially failed, freshness degrades to an hour rather than to never — and re-running the connect flow re-registers idempotently.

Worked example: abandoned-cart WhatsApp recovery

The recovery flow is four steps, and each one maps to an integration behavior above.

1. Trigger on `cart.created`. Shopify fires checkouts/create the moment a buyer enters checkout — long before any order exists. The receiver upserts the contact and emits event.type = "cart.created" carrying recovery_url (Shopify's abandoned_checkout_url), total_price, currency, and the external checkout id. That emit is what your flow subscribes to.

2. Delay. Wait long enough for the buyer to finish unassisted — an hour is a reasonable starting point. Immediate sends convert worse than they should because they interrupt people who were still checking out.

3. Exit on self-completion. Before sending, check whether an orders/create arrived with the same checkout token. If it did, exit the branch — the cart closed on its own and a recovery message is now an annoyance.

4. Send the WhatsApp recovery template. WhatsApp requires a pre-approved template for business-initiated messages, so the send is a template call with the cart fields as body parameters:

curl -X POST https://api.orbit.devotel.io/api/v1/messages/whatsapp \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "type": "template",
    "template": {
      "name": "cart_recovery",
      "language": { "code": "en" },
      "components": [
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "{{cart_total}} {{currency}}" },
            { "type": "text", "text": "{{recovery_url}}" }
          ]
        }
      ]
    }
  }'

Guest checkouts work here too. When a checkout carries only an email or phone, the receiver upserts with externalId = "checkout:<checkout_id>" — a stable handle the drip can target — and the contact row merges by email/phone when the buyer later logs in. The buyer who never created an account is still reachable, and their history consolidates when they identify.

For the full flow-definition shape, start from the order/updates recipe in Five flow recipes and substitute the cart.created trigger plus these checkout fields.

GDPR-mandated topics on the same endpoint

Shopify requires every Public app to answer three compliance webhooks, configured at the Partner-app level rather than per store. Orbit implements them on the same HMAC-verified receiver, which matters to buyers evaluating the integration: one endpoint, one signature path, one audit trail.

TopicWhat Orbit does
customers/data_requestAccept and log the request; the export is fulfilled against the published privacy policy within the 30-day SLA.
customers/redactSoft-delete the matched contact row; the tenant-scoped data-retention sweeper completes the hard redaction asynchronously.
shop/redactFires 48 hours after uninstall; marks the connection inactive and enqueues a tenant-scoped hard redaction.

All three return 200 regardless of internal outcome, so Shopify's compliance harness never retries a terminal request. The controls around them stay tenant-owned: your privacy policy, your retention sweeps, your opt-out keywords, quiet hours, and consent windows are settings you hold — the integration answers Shopify's mandated topics and leaves the posture decisions to you.

KPIs to instrument

Instrument the flow before you call it done. Five numbers cover it:

  • Recovery rate. The fraction of triggered cart.created flows that end in an order attributable to the recovery message, on an attribution window you pick and hold constant. This is the headline metric; everything else explains it.
  • Early-exit rate. The fraction of flows exiting at the self-completion check. A high exit rate with a low recovery rate means your delay is doing the converting — shorten it and see whether the message starts earning its keep. A near-zero exit rate with a low recovery rate means the message itself is weak.
  • Delivered and read rates on the recovery step. Per-recipient channel truth from the message log, split by destination market. A delivery dip in one market is a template or quality-rating problem, not a flow problem.
  • Opt-out rate on the recovery template. STOP responses attributed to the recovery send. Cart recovery lives inside a consent scope; a rising opt-out rate means the cadence or copy has overreached that scope, and the fix is the message, not the targeting.
  • Event freshness lag. Time from Shopify checkout event to flow trigger. Sustained lag means you silently fell back to the hourly poll — check the registration state on the connection record and re-run the connect flow.

Build it

The operator walkthrough — OAuth connect, auto-registration, the receiver's verification and idempotency contract, failure modes, and troubleshooting — is in the Shopify integration guide. The messaging surface the recovery step sends through is the WhatsApp channel, and the flow-definition patterns the trigger plugs into are in Five flow recipes. Connect a store, trigger on cart.created, and the first recovery send is an afternoon's work.

Frequently asked questions

Does Orbit answer Shopify's mandatory GDPR webhooks?

Yes. The three compliance topics — customers/data_request, customers/redact, and shop/redact — are implemented on the same HMAC-verified receiver as the operational topics, and all three return 200 regardless of internal outcome so Shopify's harness doesn't retry a terminal request.

What happens if Shopify's webhook delivery is missed?

An hourly poll refreshes customers, orders, products, and abandoned checkouts, so a missed delivery degrades freshness to an hour instead of losing the event. Re-running the connect flow re-registers the topics idempotently to restore the real-time path.

Do guest checkouts get recovery messages?

Yes. A checkout with only an email or phone upserts a contact under a stable checkout-id handle, so the drip can target it, and the row merges by email/phone when the buyer later logs in.

Why does a test request return 401 or 503?

Both are the receiver's expected fail-closed behavior. A wrong HMAC returns 401; an unset app shared secret returns 503 with the integration disabled. Neither silently accepts unsigned traffic.

Does the recovery flow cancel if the buyer completes the purchase?

Yes, if you build the exit check — before the send, look for an orders/create with the same checkout token and exit the branch. The order event and the checkout event land on the same contact row, so the check is a property read, not a lookup.

The takeaway

Triggered commerce messaging beats the batch calendar on freshness, self-cancellation, and identity — and the integration behind it is a verified, idempotent, audit-friendly endpoint with an hourly poll for the misses. The Shopify integration guide has the connect walkthrough; the WhatsApp channel docs carry the template-send surface the recovery step uses.

Published 28 August 2026.

Shopify Conversational Commerce — Real-Time Cart Recovery via HMAC-Verified Webhooks — Orbit by Devotel