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 athttps://mcp.shop.com/skill.md. - One connection gives an agent 17 tools. The changelog names a handful:
search_catalog,lookup_catalogandget_productfor catalog reads,complete_checkoutto finish a purchase,search_ordersfor orders and tracking, andget_checkoutfor 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: completedwith anorder”. - A
shopCLI (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:
- 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.
- Right after
complete_checkout. Payment and order creation are in flight. This is where status flips, usually fast. Poll tightly. - Long tail. Something is slow or stuck. Back off hard.
A reasonable schedule:
| Phase | Interval | Stop condition |
|---|---|---|
0–10 s after complete_checkout | 1 s | terminal status |
| 10–60 s | 2 s → 5 s, exponential with jitter | terminal status |
| 1–15 min | 30 s | terminal status |
| 15 min – 24 h | 5–15 min, background job | terminal status, or escalate |
| > 24 h | stop; mark unknown and reconcile | human 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:
submittingandconfirmingare your states, not Shopify’s. You recordsubmittingbefore callingcomplete_checkout. If the call times out you do not know whether it reached the server, so you go toconfirming, never back toawaiting_buyer.- Never re-call
complete_checkouton a timeout. Resolve withget_checkoutfirst. This is the same rule as the “timeouts resolve via read, never via blind retry” item in the OMS idempotency post. Whethercomplete_checkoutis itself idempotent is undocumented; assume it is not. completedandrecordedare separate.completedis a fact on Shopify’s side.recordedmeans 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, beforecomplete_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
orderon 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
confirmingorunknown. One intent, one in-flight checkout. - Two workers poll the same checkout and both “record” it. Guard:
shop_order_ref uniqueplus compare-and-set onstate_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_atin a durable table and a sweeper that picks up overdue rows. - The status never turns terminal. Guard: the
unknownstate with an explicit reconcile path rather than a silent timeout. - The order completed but the agent recorded failure. Guard: never mark
failedfrom 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:
- 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.
- 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. - Carry that tag into the ERP as a sales channel or custom segment so finance can split revenue, returns and chargebacks by channel.
- 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(andorders/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 CentralexternalDocumentNumber, 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 side | Merchant side | |
|---|---|---|
| Source of truth for | ”the shopper approved this" | "an order exists and was paid” |
| Signal | polling get_checkout; search_orders for signed-in shoppers | Shopify webhooks / Admin API |
| Dedupe key | purchase_intent_id, checkout_id | Shopify order id |
| Reconcile against | search_orders | ERP sales orders, payouts |
| Typical failure | stuck in confirming / unknown | missed webhook, unsynced ERP order |
Agent-side reconciliation job (proposed):
- Select intents in
unknown, orconfirmingpast their deadline. - Call
get_checkout. If terminal, transition. - If still unclear and the shopper is signed in, use
search_ordersto look for an order from that store around that time and amount. Match conservatively; a fuzzy match goes to a human, not torecorded. - 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
| Failure | Who sees it | Mitigation |
|---|---|---|
complete_checkout times out | agent | go to confirming, poll; never re-submit blindly |
| Poller crashes | agent | durable next_poll_at, sweeper |
| Throttled while polling | agent | global backoff per key, jitter |
| Status never terminal | agent | unknown + reconcile via search_orders or human |
| Agent retries with a new cart | both (duplicate order) | one in-flight checkout per intent |
| Duplicate recording | agent | unique shop_order_ref, CAS on state |
| Total differs from confirmed | both | store total_confirmed, compare on completion |
Missed orders/create webhook | merchant | hourly Admin API reconciliation |
| Agent orders unidentifiable | merchant | test order diff, explicit “unknown channel” |
| Shopper contacts agent, not store | merchant | channel tag, CS macros for agent orders |
| Non-Shopify store checkout | agent | different 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_idat shopper confirmation, beforecomplete_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 ofcomplete_checkout. - Unique constraints on
checkout_idand order reference; forward-only state transitions. -
unknownstate with a reconcile job usingget_checkoutandsearch_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_checkoutstatus values beyondcompleted. The state machine above uses my own intermediate states. - Whether
complete_checkoutis 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.