Read-only per-epic slice. The canonical source of truth is BRD.md — stories are addressed by US-ID, not by this page's line numbers.
Epic 27 — Developer platform (REST API, webhooks, SDK) (derived view)
Read-only per-epic slice of
BRD.md§9, lines 11514–11702. The canonical source of truth isBRD.md— edit there, never here. The stable address for a story is its US-ID (US-27.x), not a line number. Regenerates on everydev → mainsync viaderive-state-on-main.
- Stories (7): US-27.1, US-27.2, US-27.3, US-27.4, US-27.5, US-27.6, US-27.7
- Generated: 2026-07-01T17:48:39.076Z · as-of commit:
b083f095
Epic 27 — Developer platform (REST API, webhooks, SDK)
<!-- traceability:start:BRD:Epic-27 --><!-- traceability:end:BRD:Epic-27 -->Prototype: Webhooks · API Reference · Credentials · Delivery Log
Value: Developers can build on top of the platform without reverse-engineering the admin.
US-27.1: Public REST API
<!-- traceability:start:US-27.1 --><!-- traceability:end:US-27.1 -->Prototype: Webhooks
Phase: MVP · Priority: P0 · Effort: XL · Persona: Developer
As a Developer, I want a documented REST API for subscriptions, charges, plans, and events, so that I can build integrations and custom admin UIs.
Acceptance criteria:
- Given I have an API key scoped to a store, When I call
GET /api/v1/subscriptions?filter[status]=active, Then I get paginated JSON. - Given I have write-scope, When I POST to
/api/v1/subscriptions/:id/actions/pause, Then the subscription pauses and the response reflects the new state. - Given rate limits are exceeded, When I call, Then I get a 429 with
Retry-After.
UX notes.
- Developer portal: OpenAPI spec, interactive docs, quick-start examples in curl/JS/Python
Data contract.
- Base:
https://api.bc-subscriptions.app/v1/ - Auth:
Authorization: Bearer <api_key>with scope checking - Resources: subscriptions, charges, plans, promotions, eligibility-rules, events, webhooks, store-credits
- Standard: pagination (cursor-based), filtering (
filter[status]=active), sparse fieldsets (fields=id,status,mrr) - Errors: JSON:API-like shape with machine-readable
code+ humanmessage+request_id
Success metrics.
- Functional: all admin UI actions reproducible via API
- Product (target): ≥ 20% of Phase 2+ merchants have at least one API integration running
- Operational (target): API P95 < 500ms; SLA 99.9%
Dependencies.
- Complete feature surface
Non-functional.
- Rate limits: 1000 req/min per API key (adjustable)
- Versioning: URI version (
/v1/); breaking changes at v2; v1 supported ≥ 18 months after v2 GA - OpenAPI schema published at
/openapi.json
US-27.2: API key management in admin
<!-- traceability:start:US-27.2 --><!-- traceability:end:US-27.2 -->Prototype: API Reference
Phase: MVP · Persona: Merchant Admin / Developer
As a Merchant Admin, I want to create and scope API keys, so that my integrations use least privilege.
Acceptance criteria:
- Given I create an API key, When I pick scopes (read-subs, write-subs, read-charges, write-charges, webhooks), Then the key works only for those scopes.
- Given I revoke a key, When subsequent calls use it, Then they fail 401.
UI states.
<!-- ui-states US-27.2 -->surface: "NOT YET BUILT — forward-looking contract. Merchant Admin (React/BigDesign) developer settings page — API key management panel. Merchant creates named API keys with least-privilege scopes (read-subs, write-subs, read-charges, write-charges, webhooks), views existing keys, and revokes them; revoked keys immediately fail 401. Persona: Merchant Admin / Developer."
idle:
render: "A page titled 'API keys' with subtitle 'Manage API keys for integrations. Each key uses least-privilege scopes.' A 'Create API key' section: 'Key name' BigDesign Input and a scope checklist (five BigDesign Checkboxes labeled read-subs, write-subs, read-charges, write-charges, webhooks, each with a plain-language description). A 'Create key' primary Button (disabled until a name is entered and at least one scope is checked). Below, a table of existing keys: Name / Scopes / Created / Last used / Revoke Button."
primary_action: "'Create key' — POSTs to /api/v1/admin/api-keys and displays the generated key value once in a dismissible reveal panel (key value is never shown again after dismissal). Each row's 'Revoke' Button triggers a confirmation Modal, then DELETEs the key."
loading:
trigger: "GET /api/v1/admin/api-keys on page mount. POST /api/v1/admin/api-keys on create. DELETE /api/v1/admin/api-keys/{id} on revoke."
render: "While the key list loads, a 'Loading API keys…' Text renders in the table area. While create is in flight, the 'Create key' Button shows isLoading ('Creating…') and all form controls are disabled. While a specific key is being revoked, only that row's Revoke Button shows isLoading; other rows remain interactive."
error:
surfaced_at: "BigDesign Message(type='error') inline with role=alert, never a toast. Load failure renders above the key table ('Could not load API keys — reload to try again'). Create failure renders above the form ('Could not create key: {reason}'). Revoke failure renders scoped to the row or directly above the table."
recovery: "Load failure: reload page. Create failure: form stays populated and re-enabled; merchant corrects the key name (e.g. for a duplicate) or scope selection and resubmits. Revoke failure: Revoke Button re-enables; merchant retries."
empty:
render: "When no API keys exist, secondary Text 'No API keys yet — create one above to get started' renders in place of the table. The Create API key form above remains fully functional."
edge_status:
- status: "key just created — one-time reveal"
affordance: "A prominently styled dismissible panel (BigDesign Message or Modal) shows the generated key value with a 'Copy to clipboard' Button and warning 'This key will not be shown again — copy it now before dismissing.' Dismissing returns to the key list where the new key appears with a masked value."
- status: "key name already exists (create conflict)"
affordance: "Message 'A key with this name already exists' — merchant changes the key name and resubmits."
- status: "no scopes selected (create validation, client-side)"
affordance: "'Create key' Button remains disabled; help text below the checklist reads 'Select at least one scope to continue.' Attempted keyboard activation is blocked by the disabled state."
- status: "revoke confirmation required (destructive action)"
affordance: "Clicking 'Revoke' opens a BigDesign Modal 'Revoke this API key?' listing the key name, with a 'Revoke' danger Button and a 'Cancel' Button — the DELETE fires only on confirmation; Cancel closes the modal and returns focus to the row's Revoke Button."
inputs:
- field: "key_name"
control: "text"
label: "'Key name' — BigDesign Input with placeholder 'e.g. Retention app integration'"
validation: "Required; unique per store; max 100 characters."
- field: "scopes"
control: "checkbox"
label: "'Scopes' — BigDesign Checkbox list, one Checkbox per scope"
allowed_values: "read-subs, write-subs, read-charges, write-charges, webhooks (exactly the five AC-specified scopes); at least one required."
disabled_focus:
keyboard: "'Key name' Input is native focusable. Each scope Checkbox is a real BigDesign Checkbox (Space toggles). 'Create key' Button is a real <button> in Tab order; when validation fails it remains in tab order with aria-disabled (not removed) so keyboard users can Tab to it and read its state — it is only removed from tab order while in-flight. Each row's Revoke Button is a real <button>. Tab order follows DOM source: key name → read-subs → write-subs → read-charges → write-charges → webhooks → Create key Button → first row Revoke → next row Revoke. The revoke confirmation Modal traps focus within itself (focus moves to the Modal's 'Revoke' Button on open; Tab cycles between 'Revoke' and 'Cancel'; Escape closes and returns focus to the originating row's Revoke Button)."
US-27.3: Outbound webhook subscriptions
<!-- traceability:start:US-27.3 --><!-- traceability:end:US-27.3 -->Prototype: Credentials
Phase: MVP · Persona: Developer
As a Developer, I want to register webhook endpoints with event-type filters, so that I receive only relevant events.
Acceptance criteria:
- Given I register an endpoint for
subscription.created, When the event fires, Then a POST is sent with payload and HMAC signature. - Given the endpoint returns non-2xx, When we retry, Then exponential backoff retries up to 24 hours, after which the event is dead-lettered.
US-27.4: Headless SDK (TypeScript)
<!-- traceability:start:US-27.4 --><!-- traceability:end:US-27.4 -->Prototype: Delivery Log
Phase: MVP · Persona: Developer
As a Developer, I want typed TypeScript SDKs for subscribers (portal) and merchants (admin), so that I integrate faster with compile-time safety.
Acceptance criteria:
- Given I install
@bc-subscriptions/subscriber-sdk, When I importcreateClient({ token }), Then I get typed methods for all supported subscriber actions. - Given the API changes, When I upgrade SDK versions, Then breaking changes are announced and the SDK emits deprecation warnings.
US-27.5: GraphQL API
Phase: P2 · Persona: Developer
As a Developer, I want a GraphQL surface for complex reads, so that I fetch nested subscription+charge+order data in one request.
Acceptance criteria:
- Given I POST a query to
/graphql, When it includes nested resources, Then only requested fields are resolved.
US-27.6: App Marketplace for extensions
Phase: P3 · Persona: Developer / Merchant Admin
As a Merchant Admin, I want to install third-party extensions (retention apps, analytics, custom interventions), so that I extend the platform without custom engineering.
Acceptance criteria:
- Given an approved partner extension, When I install it, Then it registers webhooks and admin UI panels under a sandboxed permission model.
UI states.
<!-- ui-states US-27.6 -->surface: "NOT YET BUILT — forward-looking contract. Merchant Admin (React/BigDesign) app marketplace page — third-party extension discovery, permission review, install, and management under a sandboxed permission model. Merchant browses approved partner extensions, reviews required permissions (webhook event types + admin UI panel registrations), installs, and manages installed extensions. Phase P3. Persona: Merchant Admin / Developer."
idle:
render: "A page titled 'App Marketplace' with a filter bar (category BigDesign Select + keyword BigDesign Input + 'Search' Button) and a responsive card grid of approved extensions. Each card: extension name, provider name, category Badge, short description, permissions summary (e.g. 'Needs: read-subs, webhooks'), and an 'Install' Button. An 'Installed apps' tab shows currently installed extensions with an 'Uninstall' Button per row and the active permission set for each."
primary_action: "'Install' on a card — opens a permissions review panel listing the webhooks the extension will register and the admin UI panels it will inject; a 'Confirm install' Button completes installation under the sandboxed model. 'Uninstall' — opens a confirmation Modal, then removes the extension, deregisters its webhooks, and removes its admin panels."
loading:
trigger: "GET /api/v1/admin/marketplace/extensions on browse tab mount. GET /api/v1/admin/marketplace/extensions/installed on installed tab activation. POST /api/v1/admin/marketplace/extensions/{id}/install on confirm. DELETE /api/v1/admin/marketplace/extensions/{id} on uninstall confirm."
render: "While the extension list loads, an aria-busy skeleton grid (four placeholder cards with pulsing rows) renders. While install is in flight, the permissions review panel's 'Confirm install' Button shows isLoading ('Installing…') and all panel controls are disabled. While uninstall is in flight, only the affected row's 'Uninstall' Button shows isLoading; other rows stay interactive."
error:
surfaced_at: "BigDesign Message(type='error') inline with role=alert, never a toast. Browse-load failure renders above the extension grid. Install failure renders inside the permissions review panel. Uninstall failure renders scoped to the affected row."
recovery: "Browse-load failure: reload page. Install failure: 'Confirm install' re-enables; merchant retries or dismisses the panel. If the failure is a sandbox permissions violation (extension requested an unauthorized webhook event), the Message names the conflicting permission and states the extension was not installed. Uninstall failure: 'Uninstall' Button re-enables; merchant retries."
empty:
render: "When the marketplace returns zero approved extensions, a centered empty-state panel reads 'No extensions available yet — check back as the partner programme grows' with a 'Browse partner docs' link (opens developer documentation). The Installed tab shows 'No apps installed' with a 'Browse marketplace' link when none are installed."
edge_status:
- status: "extension already installed"
affordance: "'Install' Button on the browse card is replaced by 'Manage' — links to the Installed apps tab entry for that extension; prevents duplicate installation."
- status: "extension pending partner approval"
affordance: "Card shows a 'Pending approval' Badge; 'Install' Button is absent; a 'Request early access' link (feedback form) is offered so the merchant can signal interest without installing."
- status: "install requires elevated permissions — merchant must explicitly confirm"
affordance: "Permissions review panel lists each requested permission in plain language with its scope type (webhook / UI panel); 'Confirm install' is available only after the merchant has scrolled through or acknowledged all permissions — prevents silent scope grant."
- status: "install failed — requested permission outside sandboxed allow-set"
affordance: "Install error Message inside the panel: 'This extension requested a permission not available under the sandboxed model ({permission})' — extension is not installed; merchant contacts the extension provider."
inputs:
- field: "category_filter"
control: "select"
label: "'Category' — BigDesign Select filter above the extension grid"
allowed_values: "All, Retention, Analytics, Notifications, Loyalty, Custom interventions"
- field: "keyword_search"
control: "text"
label: "'Search extensions' — BigDesign Input with placeholder 'Search by name or provider'"
validation: "Optional; client-side substring filter applied to loaded extension cards; no minimum length."
disabled_focus:
keyboard: "Category Select and keyword Input are native focusable BigDesign components at the top of the page in Tab order; 'Search' Button is a real <button>. Each extension card's 'Install' (or 'Manage') Button is a real <button> — no div-onClick on cards. The permissions review panel, when open, traps focus within it: focus moves to the panel heading on open; Tab cycles through the listed permissions and 'Confirm install' / 'Cancel' Buttons; Escape closes the panel and returns focus to the originating 'Install' Button. 'Uninstall' Buttons in the Installed tab are real <button> elements in DOM order. The uninstall confirmation Modal traps focus between its 'Uninstall' (danger) and 'Cancel' Buttons; Escape closes and returns focus to the originating row."
US-27.7: Sandbox test stores
Phase: P2 · Persona: Developer
As a Developer, I want to create sandbox stores for integration development without affecting production, so that I develop safely.
Acceptance criteria:
- Given I request a sandbox, When it's provisioned, Then I get isolated store + API keys + test subscriptions.