Shop MCP Without Webhooks: Polling Agent Orders into Your OMS and ERP


In When the OMS Speaks MCP I argued that MCP gives you no idempotency and that you have to carry an intent id from the agent all the way to the ERP. In After the Buy Button I looked at what the checkout protocols leave for the back office once an order exists. Yesterday Shopify shipped something that tests both arguments in a very concrete way.

On October 13, 2026 Shopify launched Shop MCP (changelog). One line in that changelog shapes most of this post: “There are no webhooks at launch, so you need to poll get_checkout for checkout status.” If you build the agent side, you are now running a poller. If you run the store, agent orders arrive in your admin like any other order. Both sides need to agree on what happened, and neither side gets a push notification from the other.

Throughout, I mark what is documented (from the changelog or The Verge’s coverage) and what is my inference or proposed design. Shop MCP is one day old; treat the inferences accordingly.

What Shopify actually shipped

Documented, from the changelog:

  • Shop MCP is live at https://mcp.shop.com, with a skill description at https://mcp.shop.com/skill.md.
  • One connection gives an agent 17 tools. The changelog names a handful: search_catalog, lookup_catalog and get_product for catalog reads, complete_checkout to finish a purchase, search_orders for orders and tracking, and get_checkout for status. I am not going to guess the names of the other tools.
  • It works over Shopify Catalog, “over 1 billion products”, and search needs no API key. An API key from the Dev Dashboard gives higher rate limits.
  • On Shopify stores, the agent builds a cart and creates a checkout at the store that sells the product. Carts and checkout work without sign-in. With a signed-in shopper, checkout can use saved addresses and Shop Pay, and the agent calls complete_checkout “after the shopper confirms the total and payment method”.
  • On non-Shopify stores, the shopper approves the purchase in Shop and the agent receives single-use card details to pay on the store’s own checkout.
  • “Orders placed through your agent appear in the merchant’s Shopify admin like any other order.”
  • A successful test is when “the checkout returns status: completed with an order”.
  • A shop CLI (npm install -g @shopify/shop) calls the same server.

From The Verge: shoppers connect an agent to their Shop account, check out with Shop Pay inside the conversation, “Buyers must approve each purchase”, orders are “funneled back to the Shop app”, and Shopify pitches it as a “personal shopper” with rules such as budget guardrails. This follows the September Meta Muse partnership; the difference is that any agent can now do it without a partnership integration.

What is not in either source: a list of checkout status values other than completed, polling rate limits, retry semantics for complete_checkout, whether Shop MCP accepts an idempotency key, and how agent orders are labeled in the merchant admin. Those gaps are where the engineering is.

Two sides, two different problems

It helps to split the work by who you are.

Agent builder. You call Shop MCP. You need to know, reliably and exactly once, when a checkout becomes an order, so you can tell the shopper, record it in your own ledger, and stop asking. Your only signal is get_checkout (and, for signed-in shoppers, search_orders). That is a polling problem.

Merchant. You receive the order in Shopify admin. Your OMS and ERP integration probably already consumes Shopify’s own orders/create webhook or polls the Admin API. That part does not change. What changes is that you have a new channel whose orders you may want to identify, route and report separately, and whose customer conversation happens somewhere you cannot see.

The missing webhook is a Shop MCP to agent gap, not a Shopify to merchant gap. Merchants keep their normal Shopify order events (my reading of “like any other order”; I have not seen a Shop MCP order payload).

Polling cadence and backoff (agent side)

All of this section is my proposed design; Shopify documents only that you must poll.

The checkout lifecycle has phases with very different expected latency:

  1. Before buyer confirmation. The checkout exists, the shopper is reading the total. Nothing changes until a human acts. Do not poll here at all; your agent already knows, because it is the one waiting for the confirmation.
  2. Right after complete_checkout. Payment and order creation are in flight. This is where status flips, usually fast. Poll tightly.
  3. Long tail. Something is slow or stuck. Back off hard.

A reasonable schedule:

PhaseIntervalStop condition
0–10 s after complete_checkout1 sterminal status
10–60 s2 s → 5 s, exponential with jitterterminal status
1–15 min30 sterminal status
15 min – 24 h5–15 min, background jobterminal status, or escalate
> 24 hstop; mark unknown and reconcilehuman or search_orders

Rules I would hold to:

  • Jitter everything. A thousand agents finishing checkouts after the same campaign should not poll in lockstep.
  • Respect rate limits you cannot see. The changelog mentions higher limits with an API key but does not publish numbers. Treat any throttling response as a signal to widen intervals for all polls under that key, not just the one checkout.
  • Move the long tail out of the conversation. The user-facing turn should wait seconds, not minutes. After ~60 s, tell the shopper the order is being confirmed and hand the checkout id to a durable background worker.
  • Persist the poll state. If your agent process restarts mid-poll, the checkout id must still be in a table somewhere with next_poll_at. A checkout id that only lived in a conversation context is how orders get lost.

Checkout status as a state machine

Documented: status: completed with an order means success. Everything else below is inference; map it to the real values once you see them from get_checkout.

         create cart/checkout
                 │
                 ▼
        ┌─────────────────┐   shopper declines / abandons
        │ awaiting_buyer  │──────────────────────────────► abandoned
        └────────┬────────┘
                 │ shopper confirms total + payment
                 ▼
        ┌─────────────────┐
        │   submitting    │  complete_checkout called (local state)
        └────────┬────────┘
     ok / timeout│ / transport error
                 ▼
        ┌─────────────────┐  status not terminal
        │   confirming    │◄────────── poll get_checkout
        └──┬──────┬───────┘
           │      │ explicit failure status
           │      ▼
           │   failed  (payment declined, out of stock, etc.)
           │
           │ status: completed + order
           ▼
        completed ──► recorded (written to agent ledger, shopper notified)

   confirming for too long ──► unknown ──► reconcile via search_orders / human

Design points:

  • submitting and confirming are your states, not Shopify’s. You record submitting before calling complete_checkout. If the call times out you do not know whether it reached the server, so you go to confirming, never back to awaiting_buyer.
  • Never re-call complete_checkout on a timeout. Resolve with get_checkout first. This is the same rule as the “timeouts resolve via read, never via blind retry” item in the OMS idempotency post. Whether complete_checkout is itself idempotent is undocumented; assume it is not.
  • completed and recorded are separate. completed is a fact on Shopify’s side. recorded means you have persisted the order reference and notified the shopper. A crash between the two must be recoverable by re-reading, which is why the order reference write is an upsert.
  • Only transition forward. Use a version column or compare-and-set on state; a slow poll result must not overwrite a newer one.

Idempotency and dedupe

MCP itself gives no idempotency, and the changelog does not mention an idempotency key for Shop MCP. So the agent side must create one and dedupe on its own.

My proposed keys:

  • purchase_intent_id: minted by the agent when the shopper confirms, before complete_checkout. One per human approval. This is the agent-side equivalent of the intent id from the previous post.
  • checkout_id: whatever identifier the checkout carries. Unique constraint: one intent per checkout.
  • Shopify order reference from the order on the completed checkout. Unique constraint: one ledger row per order.
create table agent_checkout (
  purchase_intent_id uuid primary key,
  checkout_id        text unique not null,
  shop_domain        text not null,
  state              text not null,
  state_version      int  not null default 0,
  shop_order_ref     text unique,       -- set once on completed
  total_confirmed    numeric,           -- what the shopper approved
  currency           text,
  next_poll_at       timestamptz,
  poll_attempts      int not null default 0,
  created_at         timestamptz not null default now()
);

How orders get doubled and how this prevents it:

  • Agent retries the whole flow after an ambiguous failure. The shopper said yes once; the agent tries again with a new cart. Guard: before creating a new checkout for the same intent, check whether the old one is still confirming or unknown. One intent, one in-flight checkout.
  • Two workers poll the same checkout and both “record” it. Guard: shop_order_ref unique plus compare-and-set on state_version.
  • The shopper re-asks in a new conversation. Guard: show recent intents in the confirmation step (“you already bought this 4 minutes ago”). This is product design, not database design, but it catches the most human duplicates.

How orders get lost:

  • The poller dies. Guard: next_poll_at in a durable table and a sweeper that picks up overdue rows.
  • The status never turns terminal. Guard: the unknown state with an explicit reconcile path rather than a silent timeout.
  • The order completed but the agent recorded failure. Guard: never mark failed from a transport error; only from an explicit failure status.

Also store total_confirmed. The shopper confirmed a total; if the completed order’s total differs (I do not know whether that can happen), you want to detect it, not discover it in a chargeback.

Merchant side: identifying and attributing agent orders

Documented: orders “appear in the merchant’s Shopify admin like any other order”. Not documented: how they are marked. I have not verified whether Shop MCP orders carry a distinctive sales channel, source_name, app attribution, tag, or note attribute. Check a real test order before you build routing on any of these.

What I would do as a merchant, as inference:

  1. Place a test purchase through Shop MCP against your own store (the changelog’s step 4) and diff the resulting order against a normal storefront order: channel, source, app, tags, customer, notes, payment gateway, landing site fields.
  2. Pick whichever field is reliable and write a small rule (Shopify Flow or your OMS ingest) that tags the order, e.g. channel:agent-shop-mcp. Tags are cheap and survive into most ERP connectors.
  3. Carry that tag into the ERP as a sales channel or custom segment so finance can split revenue, returns and chargebacks by channel.
  4. If agent orders are indistinguishable, say so in your reporting rather than guessing. A mis-attributed channel is worse than an “unknown” one.

Why bother: agent orders have a different failure profile. The shopper talked to an agent you do not control, approved a total in that conversation, and may come back to the agent rather than to you with a problem. Returns, “I didn’t order this” disputes and address errors are worth tracking separately from day one.

OMS and ERP sync: reuse what you have

For the merchant, the integration path does not need to change (inference, based on “like any other order”):

  • Keep consuming Shopify’s orders/create (and orders/updated, orders/cancelled) webhooks or Admin API polling exactly as today.
  • Keep using the Shopify order id as the external id in the ERP (NetSuite externalid, Business Central externalDocumentNumber, as discussed in After the Buy Button). That is your dedupe key on the merchant side, and it is independent of anything the agent does.
  • Add the channel tag as a mapped field.

Shopify’s own webhooks are at-least-once and can be missed, so the usual hygiene applies with more force when a new channel adds volume:

  • Hourly reconciliation: list orders created in the last N hours from the Admin API, compare against ERP sales orders by external id, create what is missing.
  • Daily financial reconciliation: payouts versus ERP cash, split by channel tag. Shop Pay agent orders should settle like other Shop Pay orders, but verify that against your first payout rather than assuming.

Two reconciliations, two ledgers

There are now two independent records of the same purchase, and they never talk to each other directly:

Agent sideMerchant side
Source of truth for”the shopper approved this""an order exists and was paid”
Signalpolling get_checkout; search_orders for signed-in shoppersShopify webhooks / Admin API
Dedupe keypurchase_intent_id, checkout_idShopify order id
Reconcile againstsearch_ordersERP sales orders, payouts
Typical failurestuck in confirming / unknownmissed webhook, unsynced ERP order

Agent-side reconciliation job (proposed):

  1. Select intents in unknown, or confirming past their deadline.
  2. Call get_checkout. If terminal, transition.
  3. If still unclear and the shopper is signed in, use search_orders to look for an order from that store around that time and amount. Match conservatively; a fuzzy match goes to a human, not to recorded.
  4. Tell the shopper the truth: “confirmed”, “not placed”, or “we are checking with the store”.

Note that search_orders is documented only “for a signed-in shopper”. Guest checkouts have no documented fallback beyond get_checkout, which makes durable polling state even more important there.

Failure modes, collected

FailureWho sees itMitigation
complete_checkout times outagentgo to confirming, poll; never re-submit blindly
Poller crashesagentdurable next_poll_at, sweeper
Throttled while pollingagentglobal backoff per key, jitter
Status never terminalagentunknown + reconcile via search_orders or human
Agent retries with a new cartboth (duplicate order)one in-flight checkout per intent
Duplicate recordingagentunique shop_order_ref, CAS on state
Total differs from confirmedbothstore total_confirmed, compare on completion
Missed orders/create webhookmerchanthourly Admin API reconciliation
Agent orders unidentifiablemerchanttest order diff, explicit “unknown channel”
Shopper contacts agent, not storemerchantchannel tag, CS macros for agent orders
Non-Shopify store checkoutagentdifferent flow (single-use card); the order lives in that store’s system, not Shopify admin; reconcile separately

The last row matters: for non-Shopify stores, the changelog says the agent receives single-use card details to pay on the store’s own checkout. Nothing about that path implies an order in Shopify admin or a get_checkout status for the merchant’s order, so plan its confirmation separately (inference).

Checklist

Agent builder:

  • Mint purchase_intent_id at shopper confirmation, before complete_checkout.
  • Persist checkout state durably with next_poll_at; no in-memory-only polls.
  • Tiered polling with jitter; long tail in a background worker.
  • Timeouts go to confirming, never to a retry of complete_checkout.
  • Unique constraints on checkout_id and order reference; forward-only state transitions.
  • unknown state with a reconcile job using get_checkout and search_orders.
  • Store the confirmed total and compare on completion.

Merchant:

  • Place a test order via Shop MCP and diff it against a storefront order.
  • Tag agent orders with whatever field proves reliable; map the tag into the ERP.
  • Keep Shopify order id as ERP external id.
  • Hourly order reconciliation and daily payout reconciliation, split by channel.
  • Customer service guidance for orders placed through third-party agents.

What I could not settle

  • The full list of 17 tool names and the set of get_checkout status values beyond completed. The state machine above uses my own intermediate states.
  • Whether complete_checkout is idempotent or accepts any idempotency key.
  • Rate limits for polling, with or without an API key.
  • How Shop MCP orders are labeled in Shopify admin (channel, source, app, tags).
  • Whether and when webhooks will be added. “No webhooks at launch” suggests they may come; if they do, keep the poller as the reconciliation path anyway.