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}/graphql — createAppExtension 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}/metafields — designed, 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 code — routes/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}/orders — not 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.ts — purpose known, scope string unconfirmed in code; flag for review |
Content-Scripts scope (/v3/content/scripts) |
Inject the Stencil storefront widget | routes/admin/onboarding/storefront.ts — purpose 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 aUNIQUEpartial index oncharges.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 ourUNIQUEindex. The ADR'sidempotency_key_echoedcontract 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 barefetchwith 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 incrementingretry_attemptor entering dunning. - Dunning retry curves are separate from HTTP retry: the per-store dunning
engine defaults to
12h / 12h / 24h / 48h / 72hand a safety-net sweep runs1h / 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. See04-money-movement.mdfor the full boundary diagram. - No Checkouts v3 order creation.
POST /v3/checkouts/{id}/ordersexists but we use merchant-initiatedPOST /v2/ordersfor 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:
- Webhook registration is
POST /v3/hooks(code) — the tier map andonboarding.tslegacy references say/v2/hooks. Code is correct; the doc is stale. - Customer groups are
/v2/customer_groups(code) — the tier map lists/v3/customer-groups. Code is correct. - Promotions are REST
/v3/promotions, not Admin GraphQL. The review brief grouped promotions under GraphQL; the implementation is REST. - BC Payments rail status: code + ADR-0082 say GA and live-validated; the tier map still labels it "fixture only." Trust the code.
store_storefront_api_customer_impersonation_tokenis in the manifest per the tier map but the code explicitly avoids it — either the manifest over-grants or the doc is stale.- Processor auto-detect
bigpay.*heuristic is empirically wrong (epic-02 audit §3) — no BC method id begins withbigpay.; 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).