Skip to content

3. BigCommerce API surface map

Generated from a canonical source

This page is a read-only projection of docs/solution-review/03-bc-api-surface-map.md. Edit the canonical file, then run npm --prefix tools/project-knowledge-derive run derive.

This map is the consolidated inventory of every BigCommerce (BC) platform API the bc-subscriptions application calls, plus the OAuth scopes those calls require. It was assembled by reading the application source at commit df2b6aa1 — not from the specification prose, which in several places is stale (see the discrepancy notes at the end). Every endpoint below is quoted verbatim from a fetch() call in the code; every scope is quoted from the guard or comment in the file that uses it.

This is the first consolidated assembly of the BC surface. Until now, usage was scattered across ~20 source files and two per-epic verification audits. Those audits — bc-platform-verify-epic-01.md and bc-platform-verify-epic-02.md — remain the deeper evidence trail: they record the live-sandbox probes and BC-doc citations behind individual claims. The "evidence" column here links to those audits where they cover a family, and otherwise cites the source file so a reviewer can read the call directly.

Two conventions used throughout:

  • {hash} is the merchant's BC store hash; {id} is a resource id.
  • Acronyms are expanded on first use: PAT (Payment Access Token), IAT (Instrument Access Token), MIT (Merchant-Initiated Transaction), NTI (Network Transaction Identifier), SCA (Strong Customer Authentication).

3.1 The surface map

All Admin REST/GraphQL calls authenticate with the per-merchant OAuth access_token in an X-Auth-Token header, against the host https://api.bigcommerce.com/stores/{hash}/… unless a different host is shown.

BC API area Representative endpoints (verbatim from code) What we use it for Constraints & failure modes worth the room's attention Evidence
OAuth — install handshake POST https://login.bigcommerce.com/oauth2/token Exchange the install code for the store access_token + granted scopes; persist encrypted JSON body required (form-encoded now 403s on PCI-scope confirmation); the only BC call wrapped in retry-with-backoff oauth.ts, epic-01 audit
OAuth — iframe load / uninstall signed_payload_jwt (HS256, verified in-app) Authenticate the control-panel iframe load and the uninstall callback HMAC-SHA256 over the JWT keyed by BC_CLIENT_SECRET; our type omits nbf/jti/user.locale (non-load-bearing drift) [jwt.ts], epic-01 audit §3–4
Admin GraphQL POST /stores/{hash}/graphqlcreateAppExtension mutation Register ORDERS / PRODUCTS / CUSTOMERS admin PANEL extensions at install CARTS model is not supported by BC (verified); per-extension failure is non-fatal to install bc/graphql-client.ts, bc/app-extensions.ts
Storefront GraphQL POST https://{storefront-host}/graphql (e.g. store-{hash}.mybigcommerce.com/graphql) Subscriber storefront cart read, cart mutations, cart-level metafields, post-checkout order cartMetafields Authed with the per-merchant Storefront API token, not X-Auth-Token; there is no line-item metafield read surface (drift F2 — we JSON-encode intents into one cart-level metafield) apps/storefront-svelte/src/lib/server/{bigcommerce,cart}.ts, routes/webhooks.ts
Storefront API token mint POST /v3/storefront/api-token Mint a per-merchant storefront token (channel 1, ~1-year TTL, CORS-scoped) so Path-2 cart-metafield reads work server-side Scope-gated on store_storefront_api; mint failure is non-fatal (column left null, recovery endpoint re-mints) lib/bc-storefront-token.ts, ADR-0059
Catalog v3 GET /v3/catalog/products, …/products/{id}, …/products/{id}/variants, …/products/{id}/variants/{vid}, PUT …/products/{id}/metafields List/read products for the plan wizard; read list price for renewal charges; probe variant inventory; write per-plan config metafields Read path proxied so the OAuth token never reaches the browser; variant endpoint is the inventory probe before order materialization routes/products.ts, routes/orders.ts, routes/plans.ts, routes/webhooks.ts, scheduler.ts
Orders v2 POST /v2/orders, GET /v2/orders/{id}, GET /v2/orders/{id}/products, PUT /v2/orders/{id} Materialize the BC order for each successful charge; create the Incomplete order the charge rail needs; read cart_id / addresses / line items; transition status post-charge POST /v3/orders does not exist — v2 owns standalone order CRUD (drift F1); payment_status is server-computed, not writable (F5); billing_address must be inlined routes/orders.ts, routes/webhooks.ts, epic-01 audit
Orders v3 (sub-resources) GET /v3/orders/{id}/transactions, POST /v3/orders/{id}/payment_actions/refund_quotes, POST /v3/orders/{id}/payment_actions/refunds; POST /v3/orders/{id}/metafieldsdesigned, not yet called Read the vaulted payment-instrument token after checkout; run gateway-direct refunds Refund path was wrong three ways until live-corrected 2026-07-14 (real path is /payment_actions/refunds; item_id is the order-line id, not catalog id; payments[].provider_id is the gateway code, resolved via the quote endpoint). The order-metafields POST is a real BC endpoint but not called in coderoutes/orders.ts documents it as intended follow-up for stamping subscription metadata (subscription_id/charge_id) that POST /v2/orders can't carry inline routes/webhooks.ts, adapters/bc-payments.ts, routes/orders.ts (metafields: documented, not called)
Checkouts v3 POST /v3/checkouts/{id}/ordersnot called We deliberately do not use the checkout-driven order-create path; renewal orders are merchant-initiated via POST /v2/orders. Listed for completeness (the path exists; the code avoids it, per F1) routes/orders.ts (comment)
Carts v3 — metafields GET/POST/PUT/DELETE /v3/carts/{cartId}/metafields Subscription intent capture — read/write the JSON intent map keyed by cart line Cap is 10 metafields per client / 250 per cart — the design uses exactly one; no optimistic-concurrency token (PUT is last-write-wins) routes/cart-intents.ts, routes/webhooks.ts
Customers v3 GET /v3/customers?id:in=, GET /v3/customers/addresses?customer_id:in=, GET /v3/customers/{id}/stored-instruments Resolve subscriber identity/email at webhook time; resolve billing address for order materialization; resolve the customer's vaulted instruments Address read filters by customer_id:in=; customer-scoped stored-instruments is distinct from the store-level vault list below routes/webhooks.ts, routes/orders.ts, services/processor-resolution.ts
Stored Instruments (vault) GET /v3/payments/stored-instruments, POST /v3/payments/stored-instruments/access-tokens; browser → POST https://payments.bigcommerce.com/stores/{hash}/stored-instruments List vaulted cards (store-level, filtered client-side); mint an IAT for the hosted add-card flow The list endpoint ignores a customer_id query param — client-side filter is load-bearing; the IAT mint does not bind to customer_id (BC ignores it) so the portal session JWT is the only real security boundary; raw card goes browser→BC, never our server services/bc-vault-client.ts
BC Payments (charge rail) POST /v3/payments/access_tokens (PAT), GET /v3/payments/methods?order_id=, POST https://payments.bigcommerce.com/stores/{hash}/payments The GA off-session recurring charge: mint a PAT for the Incomplete order, match the stored instrument, charge via BigPay Requires a pre-existing Incomplete BC order (drift F3); charge is atomic-only (no per-request auth/capture split); charge-decline responses carry no processor decline code (BC platform limitation → classified soft/retry) adapters/bc-payments.ts, epic-02 audit, ADR-0082
BC Payments Transactions API GA: POST /v3/payments/transactions/{authorizations,purchases}; not GA: …/{captures,voids,refunds} (404) Would enable deferred capture / void for on-ship subscriptions Captures/voids are not GA (live-probed 404, 2026-07-14) — authorize() throws so the scheduler falls back to immediate capture; capture()/voidAuth() fail soft adapters/bc-payments.ts
Content Scripts v3 POST /v3/content/scripts, DELETE /v3/content/scripts/{uuid} Inject the storefront subscribe widget on Stencil themes at install; remove it on teardown Script name may not contain colons (422); auto-registered best-effort at install, manual wizard is the recovery path routes/admin/onboarding/storefront.ts
Promotions v3 (REST) GET/POST/PUT/DELETE /v3/promotions Reconcile an AUTOMATIC (no-coupon) product promotion so a plan's intro offer / free trial discounts the first checkout only Promotions have no custom-metadata field (our flag lives in our own promotion_settings); AUTOMATIC promotion is product-scoped, so one-time buyers get the intro price too (documented caveat) bc/promotions.ts, db.ts
Channels / Currencies / Customer Groups / Price Lists GET /v3/channels, GET /v2/currencies, GET /v2/customer_groups, GET /v2/customer_groups/{id}, GET /v3/pricelists?include=assignments, GET /v3/pricelists/{id}, GET /v3/pricelists/{id}/records Storefront enablement, currency picker, B2B customer-group targeting, B2B/tier pricing resolution /v2/currencies returns a bare array (not the v3 {data,meta} envelope); currency proxy degrades to a USD default when no OAuth token is on file routes/admin/onboarding/storefront.ts, routes/products.ts, routes/admin/bc-proxy.ts, routes/admin/pricelists.ts, routes/plans.ts, services/price-list-resolver.ts
Webhooks (inbound subscriptions) GET /v3/hooks, POST /v3/hooks Idempotently register our event subscriptions at install and via admin remediation Signature verification uses the standardwebhooks spec (not custom HMAC); full event table and handler map are in the event-backbone doc routes/webhooks.ts; full table → 05-event-backbone.md
B2B Edition POST https://api-b2b.bigcommerce.com/graphql (authorization, currentUser); GET https://api-b2b.bigcommerce.com/api/v3/io/companies Resolve a shopper's B2B company + role for the subscriber portal; probe whether B2B Edition is installed/enabled Separate host and auth model (buyer-portal JWT passthrough / X-Auth-Token + X-Store-Hash); every function degrades to null when credentials or the B2B scope are absent, so non-B2B shoppers are unaffected apps/storefront-svelte/src/lib/server/b2b-edition.ts, apps/api/src/lib/bc-b2b-edition.ts

Notes on individual families

The BC Payments charge is a three-call sequence, order-first. charge() mints a PAT for the Incomplete order (is_recurring: true), then GET /v3/payments/methods?order_id= to find the payment method whose stored_instruments[].token matches the stored instrument, then POSTs to payments.bigcommerce.com/.../payments with Authorization: PAT <token>. This inverts the "charge then materialize" shape that works for Stripe — the BC rail cannot charge a stored card outside an order (drift F3). The rail is GA and was live-validated end-to-end against sandbox cdfqf9k6zf on 2026-07-01 (ADR-0082); the tier-map's older "fixture only" status is stale.

Refunds go through Orders v3, not the Payments host. The gateway-direct refund resolves order line items via GET /v2/orders/{id}/products, gets the gateway provider_id from POST …/payment_actions/refund_quotes, then POSTs to …/payment_actions/refunds. Partial refunds on a multi-line (bundle) order fail loudly rather than guess a per-line split.

Two GraphQL surfaces, two auth models. Admin GraphQL (api.bigcommerce.com/…/graphql, X-Auth-Token) is used only for App Extensions. Storefront GraphQL (the merchant's storefront host /graphql, Storefront token) drives the subscriber cart and the cart-metafield intent carrier. They are unrelated endpoints despite both being "GraphQL."


3.2 OAuth scope inventory

Important framing: the authoritative granted-scope set lives in the BC Developer Portal app manifest, which is not checked into this repo. The scopes below are the ones the code references — either as a runtime guard, a documented requirement in the file that makes the call, or an endpoint whose BC-published scope is known. Confirming the manifest grants exactly this set (no more, no less) is an operator action and an open review item (epic-02 audit §2; tier-map open-question 5).

Scope string (verbatim) Why the app needs it Where exercised (code)
store_v2_orders Create / read / modify orders (renewal materialization, status transition, refund) routes/orders.ts, adapters/bc-payments.ts
store_v2_orders_read_only Read order status in the auth-expiry sweep cron/auth-expiry-sweep.ts
store_v2_transactions Order-transaction reads today; required for captures/voids when BC ships them GA adapters/bc-payments.ts
store_v2_products Catalog read + per-product metafield writes routes/products.ts, routes/plans.ts
store_v2_customers Customer + address reads routes/orders.ts, routes/admin/bc-proxy.ts
store_v2_information_read_only Store info / currency list routes/products.ts
store_v2_marketing Promotions CRUD (intro-offer / trial automatic promotions) bc/promotions.ts, routes/admin/promotion-settings.ts
store_app_extensions_manage Register admin App Extension panels via GraphQL bc/app-extensions.ts, bc/graphql-client.ts
store_storefront_api Mint the per-merchant Storefront API token lib/bc-storefront-token.ts (STOREFRONT_API_SCOPE)
store_payments_access_token_create Mint a PAT for the charge rail adapters/bc-payments.ts
store_payments_methods_read GET /v3/payments/methods for processor auto-detect + charge services/processor-auto-detect.ts, adapters/bc-payments.ts
store_stored_payment_instruments_read_only List vaulted instruments services/bc-vault-client.ts
store_stored_payment_instruments Mint the IAT for the hosted add-card flow services/bc-vault-client.ts
Webhook registration scope (POST /v3/hooks) Create the inbound event subscriptions at install routes/webhooks.tspurpose known, scope string unconfirmed in code; flag for review
Content-Scripts scope (/v3/content/scripts) Inject the Stencil storefront widget routes/admin/onboarding/storefront.tspurpose known, scope string unconfirmed in code; flag for review
store_storefront_api_customer_impersonation_token Appears in the manifest per the tier map, but the code deliberately avoids it in favor of store_storefront_api (impersonation authorizes impersonated checkouts, not the cart-metafield reads we need) Referenced only to exclude it — oauth.ts, lib/bc-storefront-token.ts. Purpose unconfirmed if actually granted — flag for review

Channels, customer-groups, and price-list reads (/v3/channels, /v2/customer_groups, /v3/pricelists) are exercised in code, but the code does not name their required scopes; they are covered by the v2/v3 read scopes above and should be confirmed against the manifest in the same operator pass.


3.3 Quota, rate-limit, and retry posture

What is decided.

  • Charge idempotency is enforced app-side, not gateway-side (ADR-0011). The idempotency key is ${subscription_id}:${cycle_anchor_iso}, stable across retries. The safety net is a UNIQUE partial index on charges.idempotency_key; a racing double-insert fails at SQL and is swallowed as a benign no-op ("double-tick swallow"). Note the code-vs-ADR gap: the BC Payments charge POST sends no caller-supplied idempotency key ({payment:{instrument,payment_method_id}} only), so ADR-0011 §1's claim that "the gateway dedupes on the caller-supplied key" does not hold for the BC rail — dedupe is entirely our UNIQUE index. The ADR's idempotency_key_echoed contract is specified but not yet wired.
  • Transient-HTTP retry wraps exactly one BC call. retryFetch (3 attempts, 250ms / 1s / 4s exponential, retry on 5xx / 429 / network, no-retry on 4xx) is applied only to the OAuth token exchange (oauth.ts:320). Every other BC call — products, orders, webhooks, promotions, the charge itself, refunds — is a bare fetch with manual per-call error handling. That is the current posture, stated plainly, not a recommendation.
  • 429 on the charge rail is handled specifically: a BigPay HTTP 429 raises BcPaymentsRateLimitError, which reschedules the charge at now+2h without incrementing retry_attempt or entering dunning.
  • Dunning retry curves are separate from HTTP retry: the per-store dunning engine defaults to 12h / 12h / 24h / 48h / 72h and a safety-net sweep runs 1h / 4h / 24h (ADR-0011 §5, superseding its illustrative curve).

What is not consolidated. There is no whole-surface rate-limit budget document. BC enforces per-store API rate limits, but nothing in the repo tracks a combined budget across the ~17 families above, and no shared throttle/backoff governs concurrent scheduler + webhook + admin-proxy traffic against the same store. The review room should treat this as an open item — see 07-open-decisions.md.


3.4 What we deliberately don't call

  • No BC-internal or private APIs. Every endpoint above is a documented public Admin/Storefront/Payments/B2B surface.
  • No raw payment-card endpoint from our servers. The raw-card vault (POST payments.bigcommerce.com/stores/{hash}/stored-instruments) is called by the browser under an IAT, never by our Worker — this is the PCI boundary. Our servers call the charge endpoint (/payments, with a PAT) and the vault list/IAT-mint endpoints, but never touch raw card data. See 04-money-movement.md for the full boundary diagram.
  • No Checkouts v3 order creation. POST /v3/checkouts/{id}/orders exists but we use merchant-initiated POST /v2/orders for renewal orders (drift F1).
  • B2B Edition only where B2B features engage. The B2B host is called solely to resolve company/role for the subscriber portal and to probe install state; non-B2B shoppers never reach it (every call degrades to null).

3.5 Code-vs-doc discrepancies surfaced during assembly

These were found by trusting the code over the prose. Each is a small review item:

  1. Webhook registration is POST /v3/hooks (code) — the tier map and onboarding.ts legacy references say /v2/hooks. Code is correct; the doc is stale.
  2. Customer groups are /v2/customer_groups (code) — the tier map lists /v3/customer-groups. Code is correct.
  3. Promotions are REST /v3/promotions, not Admin GraphQL. The review brief grouped promotions under GraphQL; the implementation is REST.
  4. BC Payments rail status: code + ADR-0082 say GA and live-validated; the tier map still labels it "fixture only." Trust the code.
  5. store_storefront_api_customer_impersonation_token is in the manifest per the tier map but the code explicitly avoids it — either the manifest over-grants or the doc is stale.
  6. Processor auto-detect bigpay.* heuristic is empirically wrong (epic-02 audit §3) — no BC method id begins with bigpay.; the detect feature is non-functional until the fix-site follow-up lands. Does not affect the charge rail itself (which matches on the stored-instrument token).