DEV Community

Andras Palinkas
Andras Palinkas

Posted on Originally published at cartback.shop

There is no 'abandoned' event: a developer's map of Shopify's abandoned checkout API and webhooks

The first thing most people look for when they start building on Shopify's abandoned checkouts doesn't exist: there is no webhook that fires when a checkout becomes abandoned. That one fact shapes the whole design, so here's the map I wish I'd had on day one — what Shopify actually gives you, what it doesn't, and the two things we learned the hard way while building a cart-recovery app on it.

"Abandoned" is a state, not an event

Shopify's Abandoned Checkouts documentation defines it in one sentence: "A checkout is considered abandoned after the customer has added contact information, but before the customer has completed their purchase."

Nothing fires at the moment of abandonment, because there is no such moment. In practice you subscribe to checkout events, watch for completion, and apply your own delay before treating a checkout as abandoned. The delay is yours to choose — Shopify leaves it entirely to you.

The webhooks

From the webhook topic list, the relevant topics:

Topic What it tells you
checkouts/create A checkout started. In our tests it fired within seconds of the shopper typing an email address.
checkouts/update Something changed — address, shipping, payment step. Carries completed_at once the order is placed.
checkouts/delete The checkout was removed.
orders/create The reliable completion signal: match it to the checkout and cancel any pending reminder.
carts/create, carts/update The pre-checkout cart. No contact information, so useful for analytics, not for reaching anyone.

The checkout payload has what a recovery flow needs: an id and token, the cart_token linking it to the earlier cart, email (once entered), total_price and currency, line_items, completed_at, and abandoned_checkout_url — the link that drops the shopper back into their checkout with everything still in it.

Subscriptions are declared in the app config. This is the shape we use:

[[webhooks.subscriptions]]
uri = "/webhooks/checkouts/update"
topics = [ "checkouts/create", "checkouts/update" ]

[[webhooks.subscriptions]]
uri = "/webhooks/orders/create"
topics = [ "orders/create" ]
Enter fullscreen mode Exit fullscreen mode

The two things we learned the hard way

Webhooks arrive more than once, and out of order. The handler has to be idempotent — upsert by token, and prefer completed_at over arrival order when deciding whether a checkout is still open. A checkout can look abandoned in one event and completed in the next, delivered in the opposite sequence.

Answer fast, then do the work. Shopify expects a 2xx quickly; slow handlers get retried and, eventually, the subscription is dropped. Acknowledge first, process after.

The listing endpoints are for reconciliation, not timeliness

The GraphQL abandonedCheckouts query lists abandoned checkouts (the docs say it needs the read_orders scope). It's the right tool for backfilling on install and for reconciling your own records. It is not the timely signal: in our testing the listing lagged the webhook stream by hours while the webhooks were immediate.

The REST resource (GET /admin/api/{version}/checkouts.json) still works, but Shopify marked the REST Admin API as legacy as of October 1, 2024 and requires new public apps to use GraphQL from April 1, 2025. Start new work on GraphQL.

Protected customer data

Abandoned checkouts are full of personal data, and Shopify treats it as protected customer data. The shopper's name, address, phone and email are protected fields that have to be requested individually, with data-protection requirements attached (encryption, retention limits, a review before the App Store listing is approved). The practical consequence: if a feature only needs counts, totals or line items, don't request the fields — the approval is lighter and there's less to protect. An app that actually contacts the shopper can't avoid it; we store email addresses encrypted at rest for exactly this reason.

Two more things every app in this space needs: a retention rule of its own (Shopify deletes abandoned checkouts older than three months from the admin, so your copy shouldn't outlive that by much), and handlers for the three mandatory compliance topics — customers/data_request, customers/redact, shop/redact — which the App Store checks for.

Putting it together

  1. Subscribe to checkouts/create, checkouts/update and orders/create.
  2. Upsert the checkout by token on each event; mark it complete on completed_at or a matching order.
  3. After your chosen delay, if it's still open, send the reminder using abandoned_checkout_url.
  4. Backfill and reconcile with abandonedCheckouts; expire your records on your own schedule.

Step 3's delay is the interesting design decision — and the one thing Shopify leaves entirely to you. There's a related gotcha worth knowing before you gate anything on consent: Shopify only records the checkout marketing checkbox when the shopper proceeds to Pay now, so most abandoned checkouts carry no recorded consent. I wrote that one up here.


The full version of this map, with the merchant-facing side, lives on the Cartback blog. Cartback is a cart-recovery app for Shopify built on exactly these pieces.

Top comments (0)