Skip to main content
More Shopify documentation

Shopify OAuth Pairing - How the App Store Flow Works

The ERPClaw Shopify OAuth pairing flow: six-character codes, single-use redemption, per-shop HMAC, and App Bridge session tokens. Our Worker drops the token at pairing.

This page is for merchants and reviewers who want to understand exactly how the Shopify App Store install flow works, what gets stored where, and why the design eliminates a common class of security incidents.

The five-second version

When you click Add app in the Shopify App Store, three things happen in order:

  1. Shopify and our Worker complete an OAuth handshake (offline access token issued, HMAC verified, scopes accepted).
  2. Our Worker mints a six-character pairing code and shows it to you in the embedded admin UI.
  3. You run erpclaw shopify-connect --pairing-code ABC-X9Z on your own machine. The code is redeemed, the OAuth token is forwarded to your ERPClaw (which stores it encrypted), and the Worker removes the token from its store on that first read.

After that, your ERPClaw talks to Shopify directly. The Worker is no longer in the data path.

Why pairing codes instead of polling

The most common pattern for OAuth + self-hosted clients is “tell the client to come pick up the token via OAuth callback URL.” That breaks when the client is behind NAT, has no inbound HTTPS, or is a CLI tool without a web server.

ERPClaw uses out-of-band pairing: the user copies a short code from one screen and pastes it into another. Same UX as Discord bot linking, GitHub CLI device flow, or Apple TV sign-in. Works regardless of the client’s network topology.

The code:

  • Six characters, shown as XXX-XXX, from the unambiguous alphabet ABCDEFGHJKMNPQRSTUVWXYZ23456789 (no 0/O/1/I/L)
  • Single use
  • Ten-minute TTL (Cloudflare KV expiration)
  • Cryptographically random (about 30 bits of entropy; sufficient for a 10-min single-use exchange)

If a code is leaked or guessed, the worst case is the attacker pairs to their own ERPClaw instead of yours. The token still gets deleted from our Worker. Your store is unaffected.

What lives on our Worker (and for how long)

Item Location Lifetime
Six-character pairing code Cloudflare KV pair:{code} 10 minutes max
OAuth offline access token Cloudflare KV pair:{code} Removed on first redemption, 10 minutes max
Per-shop HMAC secret Cloudflare KV pair:{code} for the handoff, and shop:{shop}:hmac to verify status pushes Until shop/redact; your ERPClaw keeps its own copy
Shop domain Cloudflare KV key names (status:, shop:, commands:) Until shop/redact
Status blob Cloudflare KV status:{shop} One hour, refreshed on each push
Queued commands (Sync now, Disconnect, compliance requests) Cloudflare KV commands:{shop} Until your ERPClaw acknowledges them, 28 days at most

Notably not on our Worker:

  • Customer data (names, emails, addresses), apart from the identifiers inside a Shopify compliance request, which wait in the command queue until your ERPClaw collects them
  • Order data (amounts, line items, products)
  • Product data (SKUs, prices, inventory)
  • General ledger entries
  • Any financial total in any form

Those live on your ERPClaw, full stop.

App Bridge v4 session tokens

The embedded admin UI talks to our Worker using App Bridge v4 session tokens (getSessionToken()). Each token is a short-lived JWT (about one minute) that Shopify signs with our app’s client secret. Our Worker:

  • Verifies the HS256 signature with that client secret, in constant time
  • Validates the aud, dest, exp, and nbf claims (with five seconds of clock-skew allowance)
  • Takes the shop identity from the verified dest claim

If you are integrating against our Worker directly (advanced use), see the JWT verification source at github.com/avansaber/erpclaw-addons.

Per-shop HMAC secret derivation

Every shop has a unique HMAC secret used to sign status pushes from your ERPClaw to our Worker. The secret is derived once via HKDF:

shop_secret = HKDF-SHA256(
  ikm:    STATUS_HMAC_MASTER_KEY,           // secret, only on Worker
  salt:   (empty),
  info:   "status-push:" + shop,            // e.g. "status-push:my-store.myshopify.com"
  length: 32 bytes
)

The master key never leaves our Worker. Each shop’s derived secret is known only to our Worker and to the merchant’s ERPClaw (delivered once during pairing).

If we ever need to rotate the master key, every existing shop’s derived secret rotates with it. Your ERPClaw will start getting 401s on status pushes until you re-pair. We do not plan to rotate without notice.

What happens on uninstall

Two webhooks fire when you uninstall the app:

  1. app/uninstalled (immediate). The Worker queues it for your ERPClaw, which flags the connection so it stops pushing status.
  2. shop/redact (48 hours later). The Worker deletes the shop’s status blob, HMAC secret, and install state, and queues the request for your ERPClaw. When your ERPClaw collects it, it deletes the Shopify connector rows for that shop (synced orders, refunds, payouts, disputes, products, customers, sync jobs).

GL entries and your core ERPClaw customer and item records are not deleted. The deletion above is carried out by your own ERPClaw when it collects the command; we have no remote way to reach into your machine.

Custom-app flow (skipping the Worker)

If you would rather not use the App Store flow at all, ERPClaw supports the Shopify Custom App pattern. See the App Store vs Custom App page for the comparison.

In short: create a Custom App in your Shopify Partners dashboard, grab the shpat_ access token, and run:

erpclaw shopify-add-account \
  --company-id 1 \
  --shop-domain my-store.myshopify.com \
  --access-token shpat_xxxxx

No Worker, no pairing code, no OAuth. You manage the token rotation yourself.

Source