references/authentication.md
# Authentication — platform keys, OAuth, restricted API keys
## Authentication
How your app authenticates and accesses Stripe data for merchants who install it.
**Canonical page:** https://docs.stripe.com/stripe-apps/api-authentication
Read this page using WebFetch before implementing authentication patterns.
## Three authentication types
Stripe Apps supports three authentication methods, configured via `stripe_api_access_type` in the app manifest:
| Auth type | Manifest value | How it works | Best for |
| --- | --- | --- | --- |
| Restricted API key (recommended) | `restricted_api_key` | Stripe generates a scoped key at install; merchant provides it to your system | Private apps, simpler integrations, apps that don’t need Connect-style access |
| Platform keys | `platform` | Your secret key + `Stripe-Account` header to act on behalf of installers | Public/marketplace apps that need to act across many merchants |
| OAuth 2.0 | `oauth` | Standard OAuth flow generates access tokens per-account | Apps where merchant must be merchant-of-record |
### Choosing the right type
**Default to restricted API keys** unless you have a specific reason to use platform keys or OAuth. RAKs are simpler, more secure (scoped permissions), and don’t create a Connect-style relationship.
Use a different type when:
1. **You need Connect webhook fanout** (events from all merchants to one endpoint): Platform keys.
2. **You’re building a public marketplace app acting across many merchants:** Platform keys.
3. **The merchant must be the merchant-of-record for charges:** OAuth.
4. **Private app or fewer merchants, no Connect fanout needed:** Restricted API keys (simplest).
## Platform keys
Your app’s API key acts on behalf of a merchant’s account using the `Stripe-Account` header:
```javascript
// Use a restricted API key when possible; fall back to secret key only for platform-key apps
const stripe = require("stripe")(process.env.STRIPE_API_KEY);
await stripe.customers.list({}, {
stripeAccount: "acct_xxxxx", // the merchant's account ID
});
```
**How to get the merchant’s account ID:**
- From a webhook event: `event.account`
- From `fetchStripeSignature` payload: the signed data includes `account_id`
- From the UI extension: `userContext.account.id` (top-level prop)
**Key fact:** Platform keys use the same `Stripe-Account` header mechanism as Stripe Connect. Installers are NOT onboarded as connected accounts in the traditional sense — the header simply authorizes your key to access their account within the app’s declared permissions.
## OAuth 2.0
Use OAuth when the connected account needs to be the merchant of record for charges, or when you need the merchant’s own Stripe identity on API calls.
Most apps do NOT need OAuth. Use platform keys unless you specifically need this.
For implementation, read: https://docs.stripe.com/stripe-apps/pkce-oauth-flow
## Restricted API keys
With RAK apps, Stripe generates a restricted key at install time with only the permissions your app declared. The merchant copies this key to your system.
Key differences from platform keys:
- No Connect-style relationship is created
- Can’t use Connect webhook fanout (each merchant manages their own webhooks)
- Simpler model for private apps or apps with fewer merchants
## Authenticating the UI to your backend (fetchStripeSignature)
`fetchStripeSignature` proves to your backend that a request came from a legitimate app installation.
Key facts:
- The signed payload contains `user_id` and `account_id` by default (field order matters)
- You can include additional data by passing it to `fetchStripeSignature(extraPayload)`
- Backend verifies with `stripe.webhooks.signature.verifyHeader()`
- The signing secret (starts with `absec_...`) is generated on first `stripe apps upload`
For the full implementation pattern, read: https://docs.stripe.com/stripe-apps/build-backend
## Identifying the installing merchant
When a merchant installs your app, Stripe sends an `account.application.authorized` event. When they uninstall, it sends `account.application.deauthorized`.
Store the account ID from `event.account` to make future API calls on their behalf.
## Permission scopes
Use the CLI to add permissions to your app:
```bash
stripe apps grant permission "customer_read" "Read customer data to show in the Dashboard"
stripe apps grant permission "event_read" "Receive webhook events"
```
This updates `stripe-app.yaml` with the correct format automatically.
**When you change permissions:** existing users must re-authorize. The app returns an invalid-request error for undeclared permissions until the user re-authorizes. See `publishing.md` for details.
references/backend.md
# Backend — when and how to add server-side logic
## When you need a backend
You need a backend if your app needs to:
- Store data long-term (user preferences, linked accounts, custom records)
- Call APIs that require server-side secrets (API keys that can’t be in the browser)
- Run logic when the user isn’t in the Dashboard (webhooks, scheduled jobs)
- Call third-party services securely (email providers, CRMs, spreadsheet APIs)
- Perform actions that take longer than the UI can wait for
**You don’t need a backend if:**
- Your app only reads and displays Stripe data (use the SDK client directly in the UI)
- You only need to store a small amount of sensitive data — use the Secret Store API instead
## Canonical documentation
Before writing backend code, read these pages using WebFetch:
| Topic | URL |
| --- | --- |
| Backend implementation + fetchStripeSignature | https://docs.stripe.com/stripe-apps/build-backend |
| Authentication types (determines backend pattern) | https://docs.stripe.com/stripe-apps/api-authentication |
| Events and webhooks | https://docs.stripe.com/stripe-apps/events |
| Secret Store API | https://docs.stripe.com/stripe-apps/store-secrets |
## Backend architecture decisions
### Authentication type determines the backend pattern
Your app’s `stripe_api_access_type` controls how the backend authenticates. See `authentication.md` for the full breakdown of auth types and when to use each one.
### CORS configuration
CORS (`Access-Control-Allow-Origin: *`) is needed ONLY on endpoints called by the UI extension. The UI runs in a sandboxed iframe with a `null` origin — specific origin allowlisting will not work.
Webhook endpoints do NOT need CORS — they receive requests from Stripe’s servers, not from the browser.
### fetchStripeSignature verification
`fetchStripeSignature` is how the UI extension authenticates requests to your backend. The signed payload and verification method are documented at https://docs.stripe.com/stripe-apps/build-backend.
Key facts:
- The signing secret (starts with `absec_...`) is generated on first `stripe apps upload`
- The default signed payload contains `user_id` and `account_id` (field order matters)
- Extra data can be included by passing it to `fetchStripeSignature(payload)`
- Verification uses `stripe.webhooks.signature.verifyHeader()`
### Webhook configuration
Webhook setup depends on your app’s distribution and auth type:
| App type | Webhook setup |
| --- | --- |
| Private (your account only) | ONE standard webhook endpoint |
| Public with platform keys | ONE webhook with “Listen to events on Connected accounts” enabled |
| Public with restricted API keys | Can’t use Connect webhook fanout — each merchant manages their own webhooks |
The `event_read` permission MUST be declared in your manifest, plus read permissions for each event type you want to receive.
Read https://docs.stripe.com/stripe-apps/events for the full setup guide.
For firewall allowlisting of inbound webhook traffic, see https://docs.stripe.com/ips for Stripe’s IP addresses.
## Secret Store API
**Plain-language:** “Stripe has a built-in secure place to store passwords, tokens, and API keys for your app — you don’t need to build your own database for secrets.”
### Two scopes
| Scope | Use for | Example |
| --- | --- | --- |
| `account` | Shared across all users of an account | The business’s API key for an email service |
| `user` | Per-user secrets | An individual user’s OAuth access token |
### Limits and restrictions
- Maximum **10 secrets per scope** (account and user separately)
- Always list and delete before adding more if approaching the limit
- Do **not** store PCI-sensitive data (card numbers, CVVs, bank account numbers)
### Declaring the permission
In `stripe-app.yaml`:
```yaml
declarations:
stripe_api_access:
permissions:
- permission: secret_write
purpose: Store third-party credentials for the app
```
### Implementation
For the correct code patterns to read, write, and delete secrets, read: https://docs.stripe.com/stripe-apps/store-secrets
## Local development with a backend
Run your backend locally alongside `stripe apps start`:
```bash
# Terminal 1: start the app preview
stripe apps start
# Terminal 2: start your backend server
node server.js
```
For webhook forwarding during local development, see `references/webhooks.md`.
references/canonical-docs.md
# Canonical documentation — sources of truth for code patterns
## Canonical documentation
Before writing any code file, read the relevant canonical docs page using WebFetch. These docs are the source of truth for API patterns, component usage, and configuration — do NOT reproduce code examples from memory or from this skill file.
If you cannot access the docs, tell the user you need them to provide the current patterns rather than guessing.
## Reference pages
| Topic | URL |
| --- | --- |
| App scaffold and workflow | https://docs.stripe.com/stripe-apps/create-app |
| Manifest schema (`stripe-app.yaml`) | https://docs.stripe.com/stripe-apps/reference/app-manifest |
| Permissions reference | https://docs.stripe.com/stripe-apps/reference/permissions |
| Backend + signed requests (`fetchStripeSignature`) | https://docs.stripe.com/stripe-apps/build-backend |
| Authentication types (platform, OAuth, RAK) | https://docs.stripe.com/stripe-apps/api-authentication |
| Events and webhooks | https://docs.stripe.com/stripe-apps/events |
| How UI extensions work | https://docs.stripe.com/stripe-apps/how-ui-extensions-work |
| UI components | https://docs.stripe.com/stripe-apps/components |
| Extensions SDK API (`createHttpClient`, Stripe client) | https://docs.stripe.com/stripe-apps/reference/extensions-sdk-api |
| Secret Store | https://docs.stripe.com/stripe-apps/store-secrets |
| Versioning and releases | https://docs.stripe.com/stripe-apps/versions-and-releases |
| Marketplace submission | https://docs.stripe.com/stripe-apps/publish-app |
| Onboarding UX patterns | https://docs.stripe.com/stripe-apps/patterns/onboarding-experience |
| Full-page apps | https://docs.stripe.com/stripe-apps/patterns/full-page-apps |
| Viewports reference | https://docs.stripe.com/stripe-apps/reference/viewports |
| Sandbox support | https://docs.stripe.com/stripe-apps/enable-sandbox-support |
## How to use this list
1. Identify which topics are relevant to the app you’re building (based on discovery answers)
2. WebFetch each relevant page BEFORE writing code
3. Follow the patterns shown in the docs exactly — field names, import paths, constructor signatures
4. If a pattern in your training data conflicts with what the docs show, the docs win
## Common lookup scenarios
| You need to… | Read this page |
| --- | --- |
| Initialize the Stripe client in a UI extension | Extensions SDK API |
| Verify `fetchStripeSignature` on your backend | Backend + signed requests |
| Choose between platform keys, OAuth, or restricted keys | Authentication types |
| Set up webhooks for a public app | Events and webhooks |
| Store secrets (OAuth tokens, API keys) | Secret Store |
| Know which UI components are available | UI components |
| Declare permissions in the manifest | Permissions reference |
| Publish to the marketplace | Marketplace submission |
references/discovery.md
# Discovery interview
## Discovery interview
Run this interview **before writing any code**. Ask one question at a time. Never use Stripe-internal jargon until after routing is complete.
### Question 1 — What do you want to do?
```
What would you like your app to do? Pick the option that sounds closest:
1. Show something or add a custom page or experience inside my Stripe Dashboard
(for example: a custom standalone page in the Dashboard, show a customer's loyalty points, add a "Send email" button)
2. Automatically do something when a payment or event happens
(for example: send a confirmation email, update a spreadsheet, sync data)
3. Both — add something to the Dashboard AND react to Stripe events
4. Let merchants connect their Stripe account to my service without sharing API keys
5. Add custom logic to how Stripe calculates bills or routes payments
(advanced — private preview)
6. I'm not sure — ask me more questions
```
**Routing:**
- Option 1 → UI extension. Ask Question 2.
- Option 2 → Backend-only app. Ask Question 3. Then read `backend.md`, `webhooks.md`, `authentication.md`, `workflow.md`.
- Option 3 → Full-stack app. Ask Question 2, then Question 3. Read all references.
- Option 4 → App-as-authentication. Read `authentication.md`, `workflow.md`.
- Option 5 → Extension interfaces (private preview). Tell the user: “This is in private preview — check [/stripe-apps](https://docs.stripe.com/stripe-apps.md) for the latest access information. I can help you get started once access is confirmed.”
- Option 6 → Ask follow-up: “What problem are you trying to solve? For example: tracking sales, notifying customers, connecting a third-party tool?”
### Question 2 — Where do you want your app to appear? (only if UI)
```
Where in the Stripe Dashboard should your app show up?
1. Next to a specific customer, payment, invoice, subscription, or product
2. Everywhere in the Dashboard as a floating side panel
3. As its own full-screen page
4. In the settings area of my app (after install)
5. As a setup guide when someone installs my app
6. I'm not sure
```
**Viewport routing:**
| Answer | Viewport(s) |
| --- | --- |
| Next to a customer | `stripe.dashboard.customer.detail` |
| Next to a payment | `stripe.dashboard.payment.detail` |
| Next to an invoice | `stripe.dashboard.invoice.detail` |
| Next to a subscription | `stripe.dashboard.subscription.detail` |
| Next to a product | `stripe.dashboard.product.detail` |
| On any list page | `stripe.dashboard.customer.list`, `.payment.list`, etc. |
| Everywhere (side panel) | `stripe.dashboard.drawer.default` |
| Full-screen page | Full-page app — `stripe.dashboard.fullpage` |
| Dashboard homepage | `stripe.dashboard.home.overview` |
| Settings | `settings` viewport |
| Setup guide (first run) | `onboarding` viewport |
If the answer is “full-screen page”, read `ui-extensions.md` (full-page apps section). If the answer is “setup guide”, also read `onboarding-ux.md`. If “I’m not sure”, ask: “When someone opens Stripe and looks at a customer’s page — would your app show up there? Or would it be more like its own separate page?”
### Question 3 — Who is this for?
```
Who will use this app?
1. Just me / my own Stripe account (private app)
2. Other Stripe users — I want to publish it to the marketplace
```
**Routing:**
- Option 1 → Private app. Simpler workflow — no marketplace submission needed.
- Option 2 → Public app. Will need account activation (verified email and business details). Note this in the plan.
### Question 3b — Authentication type (only for public apps that need backend access)
If the user chose public/marketplace AND their app needs to access merchant data from a backend, determine the authentication type. Read `authentication.md` for the full comparison — restricted API keys are the recommended default unless the app specifically needs Connect-style access or OAuth.
For private apps or frontend-only apps, skip this question — restricted API keys or platform keys both work, and RAKs are simpler.
### Question 4 — Will your app need to remember things or talk to other services?
```
Will your app need to:
1. Remember settings or store information (for example: a user's login for another service,
preferences, or data not already in Stripe)
2. Talk to another service (for example: send emails, update a spreadsheet, call a third-party API)
3. No — it will only show Stripe data
```
**Routing:**
- Option 1 or 2 → Needs backend or Secret Store API. Read `backend.md`.
- If storing credentials/tokens → use the Secret Store API (plain-language: “Stripe has a built-in secure place to store passwords and tokens — you don’t need to build your own database for secrets”)
- If running server-side logic → needs a self-hosted backend
- Option 3 → Frontend-only. Only the SDK’s Stripe client and `@stripe/ui-extension-sdk/ui` needed. No backend.
### After the interview — show a summary
Before writing any code, confirm your understanding with the user:
```
Here's what I understood:
- You want to: [plain-language description of the goal]
- Your app will appear: [where, or "on a backend server"]
- It's for: [just you / other Stripe users]
- It needs to: [remember things / talk to [service] / just show Stripe data]
Does that sound right? I'll start building once you confirm.
```
Only proceed after the user confirms. If they correct anything, update your understanding and show the summary again.
### Private preview feature detection
Some Stripe Apps features are in **private preview** — they require the user to be gated in before they can use them. Detect these during or after the interview:
**Private preview features:**
| Feature | Trigger phrases (user might say) | What to tell the user |
| --- | --- | --- |
| Custom objects | “store custom data in Stripe”, “create my own data model”, “custom database in Stripe”, “custom fields on customers”, “structured data that isn’t in Stripe already” | “Custom objects let you define your own data types in Stripe, but this feature is currently in private preview. You’ll need to have access enabled on your account before we can use it. Can you confirm you’re gated in for custom objects?” |
| Extension interfaces | “change how Stripe calculates”, “custom billing logic”, “modify payment routing”, “override Stripe’s default behavior”, “custom tax calculation” | “Extension interfaces let your app hook into Stripe’s processing pipeline, but this is in private preview. Can you confirm you have access to extension interfaces on your account?” |
**When to check:**
- If the user picks Option 5 in Question 1 → extension interfaces (already handled)
- If the user’s description of what their app does (Question 1 or free-form description) implies custom objects or extension interfaces → ask before proceeding
- If the user mentions “custom objects” or “extension interfaces” by name at ANY point → confirm access
**How to proceed after confirmation:**
- User confirms access → continue building with that feature
- User says they don’t have access → suggest alternatives:
- Instead of custom objects → use Secret Store API for key-value data, or store data in their own backend
- Instead of extension interfaces → suggest a webhook-based approach that reacts to events rather than intercepting processing
- User is unsure → tell them: “You can check your access at the Stripe Apps page in your Dashboard, or ask your Stripe account representative. I can help you build with an alternative approach in the meantime.”
## Plain-language glossary
Use these explanations when you need to introduce technical terms after routing:
| Term | Plain-language explanation |
| --- | --- |
| UI extension | The part of your app that shows up inside the Stripe Dashboard |
| Viewport | Which specific Dashboard page your app appears on |
| Extension interface | A hook that lets your app change how Stripe processes billing or payments |
| Platform keys | How your app accesses merchant data when they install it — no manual key-sharing needed |
| Connected account | A merchant who has installed your app |
| Permissions | What Stripe data your app is allowed to read or write; must be declared before use |
| Secret Store | Stripe’s built-in way for your app to save sensitive information like passwords or tokens |
| stripe-app.yaml | The configuration file that tells Stripe what your app is called, what it needs access to, and where it appears |
| Custom objects | Custom data types you define and store inside Stripe (in private preview — requires access) |
| Sandbox | An isolated Stripe test environment for safe testing — useful for testing destructive operations or onboarding flows |
references/extension-types.md
# Extension types
## Extension types
Stripe Apps supports five extension types. Use the discovery interview in `discovery.md` to determine which one the user needs.
### 1. UI extension — “show something in the Dashboard”
Renders custom UI inside the Stripe Dashboard using the Stripe UI toolkit. Runs in a sandboxed iframe.
**Plain-language examples:**
- “Show a customer’s loyalty points next to their Stripe profile”
- “Add a button to send a custom invoice email”
- “Build a full-screen analytics dashboard inside Stripe”
- “Show a customer’s order history from my store next to their Stripe data”
**What you can build:**
- Page-specific panels (next to a customer, payment, invoice, subscription, or product)
- A side panel that appears everywhere in the Dashboard
- A full-screen page inside the Dashboard
- A setup/onboarding screen when users first install the app
- An app settings page
**Key constraints:**
- React 17 only (not 18+)
- Only `@stripe/ui-extension-sdk/ui` components — no Tailwind, HTML, or third-party UI libraries
- Can’t access `window`, `document`, or `localStorage`
- Must use the SDK’s Stripe API client (see canonical docs for initialization pattern)
**Read:** `ui-extensions.md`, `workflow.md`
### 2. Backend-only app — “react to events, no Dashboard UI”
Runs on the developer’s server. Receives Stripe webhooks and calls the Stripe API. No Dashboard UI.
**Plain-language examples:**
- “Email a download link after a payment”
- “Sync purchases to a Google Sheet”
- “Create an order in my fulfillment system when a payment succeeds”
- “Notify my team on Slack when a new subscription starts”
**How it works:**
- Your server receives Stripe events (webhooks)
- Your server calls the Stripe API using platform keys (no manual key-sharing with merchants)
- No UI — all logic runs server-side
**Read:** `authentication.md`, `webhooks.md`, `backend.md`, `workflow.md`
### 3. Full-stack app — “Dashboard UI + backend server”
Combines a UI extension with a backend server. The UI can show data from external services and trigger server-side actions.
**Plain-language examples:**
- “Show my customer’s loyalty points in Stripe AND update them when they make a purchase”
- “Let merchants configure their email templates from the Dashboard, then send emails from my server”
- “Show real-time shipping status next to each payment”
**How it works:**
- UI extension in the Dashboard for user interaction
- Backend server for data storage, third-party API calls, and webhook processing
- UI authenticates to the backend using `fetchStripeSignature`
**Read:** all reference files
### 4. Extension interfaces — “plug into Stripe’s billing or payments engine” (private preview)
Lets your app change how Stripe processes billing or payments. Available types:
**Billing extensions:**
- Custom discount calculation
- Custom proration calculation
- Custom customer balance handling
- Custom recurring billing item handling
**Payments orchestration:**
- Custom payment routing
**Private preview:** Extension interfaces are not generally available. If the user asks for this:
1. Explain it’s in private preview
2. Tell them to check the Stripe Apps documentation for the latest access information
3. Ask them to check access and return when they have it
4. Do not attempt to build anything until access is confirmed
### 5. Embedded apps — “embed a third-party Stripe App inside your platform” (private preview)
For Connect platforms that want to surface third-party Stripe Apps (like QuickBooks, Xero, or Mailchimp) directly inside their own product.
**This is different from building an app.** Embedded apps are for platforms that want to *host* existing apps, not for building new ones.
**Private preview:** If the user asks for this, point them to https://docs.stripe.com/stripe-apps/embedded-apps.
## Full viewport routing table
For UI extensions — maps plain-language descriptions to viewport IDs:
| What the user wants | Viewport ID |
| --- | --- |
| Next to a specific customer | `stripe.dashboard.customer.detail` |
| On the customers list page | `stripe.dashboard.customer.list` |
| Next to a specific payment | `stripe.dashboard.payment.detail` |
| On the payments list page | `stripe.dashboard.payment.list` |
| Next to a specific invoice | `stripe.dashboard.invoice.detail` |
| On the invoices list page | `stripe.dashboard.invoice.list` |
| Next to a specific subscription | `stripe.dashboard.subscription.detail` |
| On the subscriptions list page | `stripe.dashboard.subscription.list` |
| Next to a specific product | `stripe.dashboard.product.detail` |
| On the products list page | `stripe.dashboard.product.list` |
| Everywhere in the Dashboard (side panel) | `stripe.dashboard.drawer.default` |
| As its own full-screen page | Full-page app — `stripe.dashboard.fullpage` |
| On the Dashboard homepage | `stripe.dashboard.home.overview` |
| App settings page | `settings` |
| First-run setup after install | `onboarding` |
For the full viewport reference, see https://docs.stripe.com/stripe-apps/reference/viewports.
references/feedback.md
# Feedback — submit agentic product feedback after a build
## Submit agentic feedback
After a build session in which you actually ran a toolchain command (`stripe generate app`, `pnpm build`, `stripe apps upload`), submit **exactly one** `stripe feedback` report, at the very end of the session, before you finish, based on what you observed this session. If you only advised and never ran a toolchain command, skip it. Requires Stripe CLI **1.50.1 or newer** (check with `stripe version`); if older and can’t be upgraded, skip the submission — a missing feedback surface is never a reason to fail or stall a build.
```bash
stripe feedback \
--json \
--actor agent \
--feature apps \
--sentiment <positive|negative|neutral> \
--message "<what broke or worked, concrete and specific>" \
--context "<what you were building, which path, CLI version>"
```
`--message` and `--context` must each be 10–2000 characters (`--message` at least 5 words). `--json` emits `{"id":"...","success":true}` and forces non-interactive mode — always pass it. Print the returned `id` so the user has it. Device, CLI version, OS, and terminal are attached automatically; message/context are PII-scrubbed before storage (including URLs), so don’t rely on a link to carry meaning.
## Choosing a sentiment
| What happened this session | Sentiment |
| --- | --- |
| A product bug or blocker stopped a command from working (for example, the scaffold produced a broken project) | `negative` |
| Minor friction or a workaround, but the build completed | `neutral` |
| The build ran clean, or a step worked noticeably better than expected | `positive` |
Submit positive reports too — don’t only report when something goes wrong.
## Writing a useful report
`--message` should name the command and observed result, not a general impression. `--context` should state the goal, path, and CLI version.
```bash
# Example — a scaffold defect that blocked the upload
stripe feedback --json --actor agent --feature apps --sentiment negative \
--message "stripe apps upload failed on a freshly generated app: pnpm image ran eslint against a stale eslint.config.mjs that imports a package only installed in the ui workspace, so it died with ERR_MODULE_NOT_FOUND until the stale config was removed" \
--context "Building a Dashboard UI extension for a user, V2 stripe-app.yaml workspace scaffolded with stripe generate app, Stripe CLI 1.50.1"
```
Use `--actor agent` for your own observations. If you’re instead relaying the user’s own verbatim complaint about a Stripe product, use `--actor human` and keep their words.
## What not to report
Feedback should be about a **Stripe product surface** — not model behavior, the user’s local environment, an unclear prompt, or a routine tool error you recovered from. Do not use `send_stripe_mcp_feedback` for this (it’s scoped to MCP-server tools only). Feedback is not support — it opens no ticket; keep filing real bugs in Jira and #app-platform-team too.
## When submission fails
Never fail or stall the build over a failed submission. Don’t retry on `429` (rate limited per device) — move on. For any other error, show the user the drafted `--message` and `--context` so they can submit it by hand.
references/onboarding-ux.md
# Onboarding UX — first-run user experience
## Onboarding UX
**Plain-language:** “When someone installs your app for the first time, the first thing they see is your app’s welcome or setup screen. This is called onboarding.”
Design this experience carefully — it determines whether merchants understand how to use your app or give up immediately.
**Canonical page:** https://docs.stripe.com/stripe-apps/patterns/onboarding-experience
Read this page using WebFetch for the correct component props and patterns.
## Options from simplest to most complex
### Option 1 — Zero-touch onboarding (easiest)
If your app only uses Stripe data and doesn’t need its own login, there’s nothing to set up. The app works immediately after install.
Use `fetchStripeSignature` to identify the user without a login screen — the user’s Stripe identity proves who they are.
**When to use:** When your app doesn’t need third-party credentials or a separate user account.
### Option 2 — OnboardingView component
Show a setup screen the first time the user opens the app. Use the `onboarding` viewport to show a dedicated onboarding page.
In `stripe-app.yaml`, add the `onboarding` viewport:
```yaml
ui_extension:
views:
- viewport: onboarding
component: OnboardingView
- viewport: stripe.dashboard.customer.detail
component: App
```
For the correct `OnboardingView` component props and structure, read the canonical onboarding page. Key requirements:
- Use the `OnboardingView` component (not `ContextView`) for the onboarding viewport
- Include required props like `completed`, `tasks`, and `title`
### Option 3 — SignInView component (third-party login)
If users need to log in to a third-party service (connecting their Google account, Mailchimp, etc.), use `SignInView` to guide them.
For the correct `SignInView` props and usage, read: https://docs.stripe.com/stripe-apps/patterns/onboarding-experience
Use the Secret Store API to save the resulting OAuth token. See `backend.md`.
## Critical rule: always check onboarding status in every view
Don’t assume the user went through the onboarding flow in order. They might open a payment page before completing setup.
Check at the start of every page-specific view whether onboarding is complete. If not, show a prompt directing them to complete setup.
## Storing onboarding state
Use the Secret Store API to remember whether a user has completed onboarding.
For the correct Secret Store API patterns, read: https://docs.stripe.com/stripe-apps/store-secrets
Key facts:
- Use `user` scope for per-user onboarding state
- Use `account` scope for account-wide configuration
- Maximum 10 secrets per scope
references/publishing.md
# Publishing — versioning, releases, test vs live mode, marketplace
## Publishing
How to version, release, and publish your Stripe App.
## Test mode vs live mode
**Plain-language:** “Test mode uses fake data so you can try things safely. Live mode uses real customer data. Always build and test in test mode first.”
| Mode | Data | When to use |
| --- | --- | --- |
| Test mode | Fake (test cards, test customers) | Development and QA |
| Live mode | Real customer and payment data | Production |
**Workflow:** Upload → install in test mode → test thoroughly → install in live mode.
**Do not skip test mode testing.** Even if your app looks correct locally with `stripe apps start`, you must install it in test mode and verify it works with the actual install flow before going live.
## Versioning
Bump `version` in `stripe-app.yaml` before each upload:
```yaml
id: com.example.my-app
version: 1.0.1
name: My App
```
Use semantic versioning:
- `1.0.0` — initial release
- `1.0.1` — bug fix
- `1.1.0` — new feature (backward compatible)
- `2.0.0` — breaking change or major feature
**Rules:**
- Versions must be uploaded in order — if you upload `2.0.0` before `1.0.0`, `2.0.0` won’t be available for release
- You can have multiple uploaded versions; you choose which one to install
- Stripe auto-upgrades installed users to the latest release — they don’t need to do anything **unless** you changed permissions
## Upload and release workflow
```bash
# 1. Bump version in stripe-app.yaml, then:
stripe apps upload
# 2. Go to Dashboard → Apps → your app → version history
# 3. Click the version you want to release
# 4. Click "Set as external test version" (test mode) or "Release" (live mode)
```
## When you change permissions
This is a common source of bugs. When you add new permissions:
1. Update `stripe-app.yaml` with the new permissions
2. Bump the version and upload
3. Existing users are notified by email
4. The **“Review Permissions”** button appears — but only on the **Apps workload page** ([dashboard.stripe.com/apps](https://dashboard.stripe.com/apps)), **not on the app itself**
5. The app returns an **invalid-request error** for the new permissions until the user clicks “Review Permissions” and re-authorizes
**Always warn users about this step** when you change permissions. Many users miss the notification and think the app is broken.
**How to notify users:** Consider adding a banner in your app UI that detects when a required permission is missing and guides the user to re-authorize.
## Publishing to the Stripe Apps Marketplace
For public apps — making your app available to all Stripe users.
### Requirements
Before submitting:
- Verified email address on your Stripe account
- Business details filled in (legal name, address)
- App passes [review requirements](https://docs.stripe.com/stripe-apps/review-requirements.md)
- Connect platform accounts cannot publish marketplace apps
### Submission
1. Go to [Dashboard → Apps](https://dashboard.stripe.com/apps)
2. Select your app
3. Click **Submit for review**
Stripe reviews your app for security, functionality, and compliance with their guidelines.
### Review requirements overview
- App must work correctly in test and live mode
- No prohibited content or misleading claims
- Privacy policy URL required
- Support contact required
- App icon and screenshots required
### After approval
Your app appears in the [Stripe Apps Marketplace](https://marketplace.stripe.com/). Any Stripe user can install it.
## Troubleshooting uploads
**Successful upload looks like:**
```
Uploading... Done
Your app has been uploaded to version 0.0.1.
```
**Common upload failures and fixes:**
| Error | Cause | Fix |
| --- | --- | --- |
| `Invalid manifest` / validation failed | Missing required fields or malformed YAML | Check indentation; ensure `id:`, `version:`, `name:` are present |
| `Build failed` / TypeScript errors | UI component has type/import errors | Run `pnpm build` locally first to see the exact error |
| `Version already exists` | Already uploaded this version number | Bump `version` in stripe-app.yaml (e.g. 0.0.1 → 0.0.2) |
| `Permission denied` / `Not authenticated` | CLI not logged in or wrong account | Run `stripe login` and verify with `stripe config --list` |
| `connect-src` / CSP error | App calls a URL not declared in content_security_policy | Add the URL to `content_security_policy.connect-src` in stripe-app.yaml |
| `extensions field required` | Missing `extensions: []` in stripe-app.yaml | Add `extensions: []` even if you have no backend extensions |
| `Component not found` | Viewport references a component name that doesn’t match your export | Ensure `component:` in stripe-app.yaml matches your default export name |
**Debugging steps when upload fails:**
1. Read the full error message — it usually says exactly what’s wrong
2. Run `pnpm build` to check for TypeScript/build errors locally
3. Validate your stripe-app.yaml has all required fields (id, version, name, declarations)
4. Check that file paths match (ui/src/views/App.tsx, not a renamed file)
5. If still stuck: `stripe apps upload --verbose` for detailed output
## Sandboxes for app development
Sandboxes provide isolated environments for safe app development and testing.
**Benefits of using Sandboxes:**
- Isolated from your live account — test destructive operations safely
- Each sandbox has its own app installation and signing secrets
- Useful for testing onboarding flows, uninstall/reinstall cycles, and permission changes
**How to use:**
1. Create a sandbox from Dashboard → Sandboxes
2. Run `stripe apps start` targeting the sandbox
3. Upload and install your app in the sandbox to test the full install flow
4. When ready, upload to your main account for production use
references/ui-extensions.md
# UI extensions — layout and craft
## UI extensions
UI extensions render custom UI inside the Stripe Dashboard, in a sandboxed iframe. This skill is the **opinionated layout-and-craft layer**: how to compose a full-page or drawer app so it feels native — placement, composition order, spacing, density, states, typography. It does **not** restate component APIs; those live on each component’s doc page, and they’re the source of truth.
### How to use this skill (read first)
- **Component API → fetch the component’s doc BEFORE you import it (required).** SDK components are split across **three import subpaths** — `@stripe/ui-extension-sdk/ui`, `/ui/next`, and `/ui/experimental` — and importing from the wrong one yields an `undefined` component and a **hard crash** (`Element type is invalid`). You can’t tell a component’s subpath from its name — for example `DataTable` and `DetailPage` are under `/ui/experimental` and the charts under `/ui/next`, not the `/ui` you’d expect. So for **every** component you use: (1) **fetch its doc** — `https://docs.stripe.com/stripe-apps/components/<name>.md` (append `?app-sdk-version=9Next` for `Tabs`, `LineChart`, `BarChart`; discover components from the [index](https://docs.stripe.com/stripe-apps/components.md)); (2) **copy the exact import line and required props / data shape** char-for-char; (3) **re-check every import against the doc before you finish.** Don’t infer an API from the component name — a wrong import path, prop, or data shape is a hard runtime error and the #1 reason these apps don’t render.
- **Layout, styles, composition, and states → follow the codified rules here (§2–§3).** These are Stripe’s craft defaults; no single component doc covers them. This is what the skill adds on top of the docs.
### Constraints — the sandbox (these cause silent failures or crashes)
UI extensions run in a **sandboxed iframe on React 17.0.2**. Only SDK components render. Don’t reach for these:
| Blocked | Use instead |
| --- | --- |
| Any HTML tag (`<div>`, `<span>`, `<button>`, `<input>`, `<form>`, `<h1>`…) | SDK components only (`Box`, `Button`, `TextField`, …) |
| CSS / Tailwind / MUI / styled-components / any stylesheet | the `css` prop with design tokens (§3) |
| React 18+ APIs — `useId`, `useTransition`, `useDeferredValue`, concurrent features | React 17 hooks only (Stripe Apps run **React 17.0.2**) |
| `window`, `document`, `localStorage`, `sessionStorage` | not available in the iframe |
| `react-hook-form` / any ref-based form library | uncontrolled inputs — `defaultValue` + `onChange` (see Forms, §3) |
| arbitrary `fetch()` to external URLs | `fetchStripeSignature` for your backend; the SDK client for Stripe APIs |
**Data access (for apps that read Stripe data — the examples here use mock data).** Initialize the client with `createHttpClient` from `@stripe/ui-extension-sdk/http_client` plus the `STRIPE_API_KEY` constant (a **sentinel, not a real key** — it uses the app’s granted permissions), then call standard SDK methods. **Every resource you call must be declared as a permission** (`stripe apps grant permission …`) or the request fails with an invalid-request error. The current object is `environment.objectContext` (for example, `.id` = `"cus_…"`); the signed-in user is **`userContext`, a top-level prop — *not* nested under `environment`**. Full rules: [how UI extensions work](https://docs.stripe.com/stripe-apps/how-ui-extensions-work.md) · [Extensions SDK API reference](https://docs.stripe.com/stripe-apps/reference/extensions-sdk-api.md).
## 1. Placement — pick your viewport
Decide *where in the Dashboard* the app lives; that determines the viewport and the root component. Full viewport list: [viewports reference](https://docs.stripe.com/stripe-apps/reference/viewports.md).
| Your goal | Surface | Viewport | Root component |
| --- | --- | --- | --- |
| A dedicated workspace: tabs, lists, dashboards, multi-step workflows | **Full-page** | [`stripe.dashboard.fullpage`](https://docs.stripe.com/stripe-apps/reference/viewports.md) | [`FullPageView`](https://docs.stripe.com/stripe-apps/components/fullpageview.md) |
| Contextual info/actions tied to a specific object (a customer, a payment) | **Page-specific** | [`stripe.dashboard.customer.detail`, `.payment.detail`, `.list`, `.overview`, …](https://docs.stripe.com/stripe-apps/reference/viewports.md) | [`ContextView`](https://docs.stripe.com/stripe-apps/components/contextview.md) |
| Available on every Dashboard page | **Dashboard-wide drawer** | [`stripe.dashboard.drawer.default`](https://docs.stripe.com/stripe-apps/reference/viewports.md) | [`ContextView`](https://docs.stripe.com/stripe-apps/components/contextview.md) |
| App configuration | **Settings** | [`settings`](https://docs.stripe.com/stripe-apps/reference/viewports.md) | [`SettingsView`](https://docs.stripe.com/stripe-apps/components/settingsview.md) |
| First-run setup after install | **Onboarding** | [`onboarding`](https://docs.stripe.com/stripe-apps/reference/viewports.md) | [`OnboardingView`](https://docs.stripe.com/stripe-apps/components/onboardingview.md) |
Rules of thumb: lead with **full-page** when the app is a destination with more than one section; use a **page-specific** drawer when the value is glanceable context on an existing object; only use `drawer.default` when the app truly applies everywhere. A full-page app can also register drawer/page-specific views — link between them.
## 2. Composition — the build order
The order and *which component does which job* (the API of each is in its linked doc).
**Full-page app** (walkthrough: [full-page apps pattern](https://docs.stripe.com/stripe-apps/patterns/full-page-apps.md)):
1. **Manifest** — register the `stripe.dashboard.fullpage` [viewport](https://docs.stripe.com/stripe-apps/reference/viewports.md) → your view. *(The CLI’s `add view` adds a full-page view to an existing app; the full-page view needs `@stripe/ui-extension-sdk` ≥ 9.2.)*
2. **Shell** — [`FullPageView`](https://docs.stripe.com/stripe-apps/components/fullpageview.md); the header (app name + icon) comes from `stripe-app.json`. Add one `pageAction` only if there’s a single clear top-level action.
3. **Routing** — `createRoutes` + `AppRouter`; read the route with `useAppRoute`, navigate with `useNavigation`. Use a `/:tabId?` pattern so tabs are bookmarkable ([routing](https://docs.stripe.com/stripe-apps/routing.md)).
4. **Tabs** — [`Tabs`/`Tab`](https://docs.stripe.com/stripe-apps/components/tabs.md) for top-level sections. Distinct areas only; don’t nest tabs.
5. **Overview** — [`OverviewPage`](https://docs.stripe.com/stripe-apps/components/overviewpage.md) with a `primaryColumn` (main content, charts) and a `secondaryColumn` (supporting modules). Group content into `PageModule`s with titles; lead with a summary. *(See the [OverviewPage doc](https://docs.stripe.com/stripe-apps/components/overviewpage.md) for the exact column/`PageModule` parent-child contract.)*
6. **List** — [`DataTable`](https://docs.stripe.com/stripe-apps/components/datatable.md): sortable columns, status cells, row → detail route, pagination, and an empty state.
7. **Detail** — [`DetailPage`](https://docs.stripe.com/stripe-apps/components/detailpage.md) with `breadcrumbs` back to the list and two columns. The tab bar isn’t visible here; the breadcrumb is the way back.
8. **Create / edit** — [`FocusView`](https://docs.stripe.com/stripe-apps/components/focusview.md) drawer over the current view.
**Drawer / page-specific app:** root is [`ContextView`](https://docs.stripe.com/stripe-apps/components/contextview.md); keep it **single-column and dense** (a drawer is narrow — don’t force multi-column). Use `environment.objectContext` for the current object. If you also have a full-page experience, link out to it rather than cramming a workflow into the drawer.
## 3. Layout and style rules (the codified craft)
These are the defaults that make an app feel native — they are *not* in any single component doc, so follow them here. Each is tagged **[Required]** (breaks/looks wrong otherwise), **[Recommended]** (Stripe’s craft default), or **[Optional]** (a style choice). Full styling reference: [style your app](https://docs.stripe.com/stripe-apps/style.md).
**[Required] `css` values are tokens, not web CSS.** The `css` prop is not CSS. Every value is a design token or fraction, never a raw unit:
- **Spacing** (`padding`, `margin`, `gap`) → tokens only (`xxsmall`…`xxlarge`). Never `"24px"`, `"1rem"`, `%`.
- **Layout** → `stack: "x" | "y"` with `gap`. There is no `display: "flex"`/`"grid"`.
- **Width** → a fraction (`"1/2"`, `"1/3"`, …) or `"fill"`. **Height** → a bare number for pixels (for example, `height: 180`).
- **Color/background** → semantic tokens (`backgroundColor: "surface" | "container"`, `color: "secondary"`), not hex.
Passing a raw CSS value (px, `flex`, hex) is a hard runtime error — the #1 way a naive build crashes. ([style reference](https://docs.stripe.com/stripe-apps/style.md))
**[Recommended] Spacing — Stripe’s token scale, tighter = more related.** Spacing (`padding`/`margin`/`gap`) uses Stripe’s fixed token scale — match these defaults, never raw px. Use the *smallest* gap that still separates things:
| Token (value) | Default use |
| --- | --- |
| `xxsmall` (2px) | label → its value; tightest intra-element spacing |
| `xsmall` (4px) | icon → adjacent text; spacing inside a chip/badge |
| `small` (8px) | between sibling cards/tiles in a row |
| `medium` (16px) | padding inside a card/module; between fields in a column |
| `large` (24px) | between distinct sections of a page |
| `xlarge` (32px) | between the two major columns of a layout |
| `xxlarge` (48px) | rarely — a major page break |
**[Recommended] Content aligns to the tab’s left edge — no wrapper padding.** The `Tabs` bar and `FullPageView` already set the page’s content edge. Don’t wrap a tab’s panel content in a `Box` with `padding` (or `paddingX`/`paddingLeft`) — that inset pushes content off the tab’s left edge and breaks alignment with the tab labels above it. Use `stack: "y"` + `gap` for vertical rhythm between modules instead; content stays flush to the same left edge as the first tab.
```tsx
// Incorrect — inset; content no longer aligns to the tabs
<Box css={{ stack: "y", gap: "large", padding: "large" }}>…</Box>
// Correct — flush to the tab's left edge
<Box css={{ stack: "y", gap: "large" }}>…</Box>
```
**[Recommended] Page structure — one consistent column layout, `OverviewPage` rendered directly.** Render `OverviewPage` **directly as the tab’s content** — not wrapped in a `Box`, and never with a full-width band stacked above it. `OverviewPage` *is* the layout; pick its shape by whether you pass `secondaryColumn`:
- **One column** → `primaryColumn` only (renders full-width).
- **Two column** → `primaryColumn` + `secondaryColumn`. **Never mix the two** — no full-width KPI row or band above a two-column split. The KPI stat row is the **first `PageModule` of `primaryColumn`** (full-width in one-column mode, primary-column width in two-column mode), *not* a separate row above the component. Group every module into the columns; don’t build a manual column layout.
**[Required] `DetailPage` is its own root route — never inside `FullPageView`.** A detail is a separate route you navigate to (for example, `route("/members/:memberId", …)`) that renders `DetailPage` at the root. `DetailPage` owns its page shell; nesting it in `FullPageView` double-stacks the header. The breadcrumb — not the tab bar — is the way back.
**[Recommended] Overview composition & density — fill the page.** An overview must read as a *dense, width-filling dashboard*, not a short column of big cards. This is the #1 thing that makes an overview look un-native, so compose it deliberately:
1. **Top: a horizontal KPI stat row** — 3–5 equal tiles side by side (see Stat tiles). **Never stack KPI cards vertically full-width** (one metric per row) — a column of oversized single-metric cards wastes the page and reads as un-native.
2. **Below: use both columns.** With `OverviewPage`, put the primary module (a trend `LineChart`, or the main list/table) in `primaryColumn` and supporting modules in `secondaryColumn`; otherwise split with `stack: "x", gap: "xlarge"` into a wider left (`width: "2/3"`) and a narrower right (`width: "1/3"`). Don’t leave half the width empty.
3. **Derive enough views to fill it.** If the data is only a few metrics, add the breakdowns, trends, top-N lists, and recent-activity the data implies (for example, points-over-time trend, members-by-tier breakdown, top members, recent redemptions) rather than leaving whitespace. Aim for **3+ modules** that fill the viewport.
Avoid: a single column of oversized full-width cards; a large empty right side or lower page; one metric per row. Match the density of a native Dashboard overview.
**[Recommended] List pages — the table is the hero.** A dedicated list/directory page (for example, a Members tab) is **the table itself**, full-width, as the primary content. The only things around it: **search / filters** (and segment tabs) *above* the table, **pagination** below, and an **empty state**. **Do not put KPI stat tiles, charts, or dashboard modules on a list page** — those belong on the overview. A native list page is dense with *rows*, not decorated with summary cards on top. Keep it: controls → full-width table (many rows) → pagination. (Overviews are multi-module and dense; list pages are single-purpose and focused — don’t blur the two.)
**[Recommended] Stat tiles — a row of top-line KPI cards.** A **single row of equal `surface` cards** (not a 2×2 grid), each a muted `caption` label above a large `semibold` value — use the card treatment from Cards & trays. Lay them out as a horizontal row with equal widths:
```tsx
// row wrapper: <Box css={{ stack: "x", gap: "medium" }}> … one card per KPI …
<Box css={{ width: "fill", stack: "y", gap: "xxsmall", padding: "medium", borderRadius: "medium", backgroundColor: "surface" }}>
<Inline css={{ font: "caption", color: "secondary" }}>{label}</Inline>
<Inline css={{ font: "subtitle", fontWeight: "semibold" }}>{value}</Inline>
</Box>
```
Aim for ~3–5 cards in one row (for example, Total spend · MRR · Refunds · Disputes). For *proportional* data (a total split into parts), prefer a progress/`MeterChart` treatment over a chart — see Charts.
**[Recommended] Two-column detail (key/value).** Outer `stack: "x", gap: "xlarge"`; each column `width: "1/2", stack: "y", gap: "medium"`; each field `stack: "y", gap: "xxsmall"` with a `semibold` label above a regular value.
**[Recommended] Charts & data viz — pick the representation that fits the data.**
- **Sizing:** a chart needs an explicit height — wrap it in a [`Box`](https://docs.stripe.com/stripe-apps/components/box.md) with a pixel height (`~180` per the [chart-layout pattern](https://docs.stripe.com/stripe-apps/patterns/chart-layout.md)) inside a `PageModule`.
- **Trend over time → [`LineChart`](https://docs.stripe.com/stripe-apps/components/linechart.md).** Use a sensible granularity (monthly or weekly); **daily points over a long range render as an unreadable, noisy line.**
- **A small breakdown / a total split into parts (for example, members-by-tier) → a `List` of rows** (or a [`MeterChart`](https://docs.stripe.com/stripe-apps/components/meterchart.md) for a proportional bar). A `BarChart` with only a few categories renders as a lonely narrow bar in an empty module — so use a list:
```tsx
import { List, ListItem, Inline } from "@stripe/ui-extension-sdk/ui";
<List>
{tiers.map((t) => (
<ListItem key={t.name} id={t.name} title={<Inline>{t.name}</Inline>} value={<Inline>{`${t.count} members`}</Inline>} />
))}
</List>
```
Reserve [`BarChart`](https://docs.stripe.com/stripe-apps/components/barchart.md) for genuine multi-bar / time-series data, and let it fill width.
- **Read the component’s doc for the exact `data` shape before wiring** — charts are strict (wrong shape = hard runtime error).
- **[Optional]** a `surface`/`container` background makes a chart read as a card; not required.
**[Recommended] Typography.** `font` accepts **only** these presets — don’t invent values (`"heading4"`, `"title2"`, and similar are not valid and crash): `body`, `bodyEmphasized`, `caption`, `heading`, `subheading`, `subtitle`, `title`, `kicker`, `lead`. `fontWeight` accepts **only** `regular` | `semibold` | `bold`. For emphasis use `fontWeight: "semibold"`; use `regular` for body. Don’t use `fontWeight: "bold"` (the SDK accepts it, but Stripe’s design language reserves it — `semibold` is the native emphasis weight). Labels are `font: "caption"` + `color: "secondary"`. ([style reference](https://docs.stripe.com/stripe-apps/style.md))
**[Recommended] Cards & trays — a background implies a radius.** When a `Box` should read as a card or tray, set surface and radius together: a **card** = `backgroundColor: "surface"` + `borderRadius: "medium"` + `padding: "medium"`; group related cards on a **tray** = `backgroundColor: "container"` + `borderRadius: "medium"` + `padding: "small"`. `borderRadius` accepts `none | xsmall | small | medium | large | rounded`; `medium` is the card default. A plain layout `Box` that isn’t a card gets no background or radius.
**[Recommended] Loading.** Put the loading state *inside* the tab/content region so the header and tab bar stay visible — don’t wrap `Tabs` or the whole view in a loading state. Center a [`Spinner`](https://docs.stripe.com/stripe-apps/components/spinner.md) ([loading pattern](https://docs.stripe.com/stripe-apps/patterns/loading.md)).
**[Recommended] Empty states.** Give [`DataTable`](https://docs.stripe.com/stripe-apps/components/datatable.md) an empty state, and swap it by scenario: an object with a call to action when there’s genuinely no data; a plain string when active filters produce zero results ([empty-state pattern](https://docs.stripe.com/stripe-apps/patterns/empty-state.md)).
**[Required] Forms are uncontrolled.** There is no `react-hook-form` or ref-based forms in the sandbox. Use **uncontrolled inputs** — `defaultValue` + `onChange` (or a plain React-17 `useState` controlled value) — for [`TextField`](https://docs.stripe.com/stripe-apps/components/textfield.md), [`Select`](https://docs.stripe.com/stripe-apps/components/select.md), and similar. A ref-based form library won’t work.
## 4. Component index
**The complete, authoritative catalog is [docs.stripe.com/stripe-apps/components](https://docs.stripe.com/stripe-apps/components.md)** — every component, grouped by **Views · Layout · Navigation · Content · Forms · Charts**. Start there to find the right component for anything not covered below (there are ~40; the table here is a curated shortcut for the common full-page jobs, **not** exhaustive). Then open that component’s own doc for its API. Pick by the job; **read the doc for the API** (props, data shape, allowed parents/children).
| Job | Component | When to use | Doc |
| --- | --- | --- | --- |
| Root of a full-page app | `FullPageView` | Full-page viewport; header from manifest | [doc](https://docs.stripe.com/stripe-apps/components/fullpageview.md) |
| Root of a drawer / page-specific view | `ContextView` | Narrow, single-column, dense | [doc](https://docs.stripe.com/stripe-apps/components/contextview.md) |
| Top-level sections | `Tabs` / `Tab` (`ui/next`) | Distinct workflow areas; route-driven | [doc](https://docs.stripe.com/stripe-apps/components/tabs.md) |
| Overview dashboard | `OverviewPage` + `PageModule` | Two-column summary; group content in modules | [doc](https://docs.stripe.com/stripe-apps/components/overviewpage.md) |
| List of objects | `DataTable` | Sortable, status cells, row→detail, empty state, pagination | [doc](https://docs.stripe.com/stripe-apps/components/datatable.md) |
| Single object detail | `DetailPage` (+ `PropertyList` for key/value) | Breadcrumb + two columns; top-level page, not inside `FullPageView` | [detail](https://docs.stripe.com/stripe-apps/components/detailpage.md) · [propertylist](https://docs.stripe.com/stripe-apps/components/propertylist.md) |
| Create / edit | `FocusView` | Overlay drawer; `Button pending` on save | [doc](https://docs.stripe.com/stripe-apps/components/focusview.md) |
| Data visualization | `LineChart` / `BarChart` / `MeterChart` / `Sparkline` (`ui/next`) | In a fixed-height `Box` in a `PageModule`; **read the doc for the `data` shape** | [line](https://docs.stripe.com/stripe-apps/components/linechart.md) · [bar](https://docs.stripe.com/stripe-apps/components/barchart.md) |
| Layout / spacing | `Box`, `Inline` | The `stack`/`gap`/`padding` substrate (see §3) | [doc](https://docs.stripe.com/stripe-apps/components/box.md) |
| Actions | `Button` | Primary/secondary; `pending` for async | [doc](https://docs.stripe.com/stripe-apps/components/button.md) |
| Loading | `Spinner` | Center in the content region | [doc](https://docs.stripe.com/stripe-apps/components/spinner.md) |
Full catalog: [all components](https://docs.stripe.com/stripe-apps/components.md) · [design patterns](https://docs.stripe.com/stripe-apps/patterns.md)
references/webhooks.md
# Webhooks — event delivery for Stripe Apps
## Webhooks
How your Stripe App receives and processes events (payments, customers, installs, etc.).
**Canonical page:** https://docs.stripe.com/stripe-apps/events
Read this page using WebFetch before implementing webhook handlers.
## Webhook configuration depends on app type
| App type | Auth type | Webhook setup |
| --- | --- | --- |
| Private (your account only) | Any | ONE standard webhook endpoint |
| Public/marketplace | Platform keys | ONE webhook with “Listen to events on Connected accounts” enabled |
| Public/marketplace | Restricted API keys | Can’t use Connect webhook fanout — each merchant manages their own |
A second test-mode endpoint is recommended for public apps but is not required.
## Required permissions
The `event_read` permission MUST be declared in your manifest for webhook event access, plus read permissions for each event type. Use the CLI to declare permissions:
```bash
stripe apps grant permission "event_read" "Receive webhook events"
stripe apps grant permission "payment_intent_read" "React to successful payments"
stripe apps grant permission "customer_read" "React to customer changes"
```
## Webhook handler requirements
For every webhook handler:
1. Use `stripe.webhooks.constructEvent()` to verify signatures
2. For public platform-key apps: check `event.account` to identify which merchant triggered the event
3. Use `stripeAccount` option to act on behalf of merchants (platform keys only)
## Local development
### Private app (events from your own account)
```bash
stripe listen --forward-to localhost:<PORT>/webhook
```
### Public platform-key app (events from connected accounts)
```bash
stripe listen --forward-connect-to localhost:<PORT>/webhook
```
**Important:** `--forward-to` only captures your own account’s events. Use `--forward-connect-to` for connected account events.
## Triggering test events
```bash
# Private app:
stripe trigger payment_intent.succeeded
# Public app (simulates connected account event):
stripe trigger --stripe-account payment_intent.succeeded
```
## Verifying webhook signatures
Always verify signatures to ensure the request came from Stripe. For the complete webhook verification pattern, read: https://docs.stripe.com/stripe-apps/build-backend
Key implementation facts:
- Use `stripe.webhooks.constructEvent()` with the raw request body and your webhook signing secret
- For platform-key apps, check `event.account` to identify which merchant triggered the event
- Use a restricted API key when possible (see `authentication.md`); use the secret key only for platform-key apps
- Return 200 quickly; process asynchronously if needed
## Handling installs and uninstalls
| Event | When it fires | What to do |
| --- | --- | --- |
| `account.application.authorized` | A merchant installs your app | Store the merchant’s account ID |
| `account.application.deauthorized` | A merchant uninstalls your app | Clean up stored data |
## Setting up webhooks in the Dashboard
1. Go to [Dashboard → Developers → Webhooks](https://dashboard.stripe.com/webhooks)
2. Click **Add endpoint**
3. Enter your endpoint URL
4. Select events to listen for
5. For public platform-key apps: check **“Listen to events on Connected accounts”**
6. Copy the signing secret to your environment variables
During local development, use `stripe listen` instead.
references/workflow.md
# Workflow — end-to-end build order
## MANDATORY — Full development loop (quick reference)
Follow this exact sequence for every new app. Do NOT skip or reorder steps.
```
1. stripe plugin install apps && stripe plugin install generate ← one-time CLI setup
2. stripe generate app <name> && cd <name> ← scaffold (NOT `stripe apps create`)
3. pnpm install ← install deps
4. [modify scaffolded files + create missing ones] ← implement (only add what scaffold doesn't provide)
5. pnpm build ← compile UI (skip for backend-only apps)
6. pnpm test ← run tests
7. stripe apps start ← local preview in Dashboard
8. stripe apps upload ← publish version (REQUIRED before Secret Store or fetchStripeSignature work)
9. Install in test mode from Dashboard → Apps ← test the installed app
10. Dashboard → Apps → Submit for review ← marketplace publishing (optional)
11. stripe feedback ← one report per build session (see references/feedback.md)
```
**BLOCKED:** Do NOT use `stripe apps create` — it does not scaffold correctly. Always use `stripe generate app`.
**MANDATORY:** Do NOT create files manually when `stripe generate app` provides them. The scaffold creates a V2 workspace: `stripe-app.yaml`, `package.json`, `pnpm-workspace.yaml`, and `ui/src/views/App.tsx` with the correct structure. Only create files that the scaffold doesn’t provide (e.g., `server.js` for your backend). Modify scaffolded files as needed — don’t rewrite them from scratch.
## End-to-end build order (detailed)
Follow this sequence exactly. Deviating from it is the #1 source of confusion when building Stripe Apps.
### Step 1 — Prerequisites (one-time setup)
Install the Stripe CLI, then install the required plugins:
```bash
# Install the apps plugin (creates and manages apps)
stripe plugin install apps
# Install the generate plugin (scaffolds new apps)
stripe plugin install generate
```
**Plain-language:** “These are tools that let the Stripe CLI create and manage apps. You only need to do this once.”
Verify your CLI version is 1.25.0 or newer:
```bash
stripe version
```
### Step 2 — Create the app
```bash
stripe generate app <your-app-name>
cd <your-app-name>
```
This creates a new V2 workspace with the correct directory structure, `stripe-app.yaml` manifest, and example UI extension.
**What gets created:**
```
<your-app-name>/
├── stripe-app.yaml # V2 app manifest (YAML) — name, permissions, viewports
├── package.json # workspace root
├── pnpm-workspace.yaml # declares workspace packages
├── ui/
│ ├── package.json
│ └── src/
│ └── views/
│ └── App.tsx # main UI component
├── extensions/ # script extensions (one subdir per extension)
└── README.md
```
### Step 3 — Install dependencies
```bash
pnpm install
```
### Step 4 — Build and test (UI apps)
For apps with a UI extension, compile TypeScript and run tests:
```bash
pnpm build
pnpm test
```
Backend-only apps without TypeScript can skip this step.
### Step 5 — Develop locally
```bash
stripe apps start
```
**Plain-language:** “This opens your app live in your Stripe Dashboard while you build it. Changes you save show up immediately — you don’t need to upload anything yet.”
**What this does:**
- Opens a browser to your Stripe Dashboard with your app running live
- Watches for file changes and hot-reloads
- Works against your live or test Stripe account
**Notes:**
- `stripe apps start` requires browser access; Safari is not supported — use Chrome or Firefox
- This does **not** persist — your app is only visible while the command is running
- The app is not installed on your account yet; it’s only previewed locally
### Step 6 — Upload a version (when ready to share or test permissions and secrets)
```bash
stripe apps upload
```
**What this does:**
- Creates a new version of your app in the Stripe Dashboard
- Generates the signing secret needed for `fetchStripeSignature` and the Secret Store API
- Makes the version available to install
**After uploading:**
1. Go to [Dashboard → Apps](https://dashboard.stripe.com/apps)
2. Find your app
3. Click **Install in test mode** to install it on your account
**When you need to upload before `stripe apps start`:**
- Using the Secret Store API
- Using `fetchStripeSignature` to authenticate the UI to a backend
- Testing permissions that require the app to be installed
### Step 7 — Install in live mode (when ready to use with real data)
1. Go to the [Dashboard → Apps page](https://dashboard.stripe.com/apps)
2. Select your app
3. Choose “Private to your account”
4. Select the version to install
5. Click Install
**Plain-language:** “Test mode uses fake data so you can try things safely. Live mode uses real customer data. Always test in test mode first.”
### Step 8 — Ship a new version
1. Bump `version` in `stripe-app.yaml` (use semantic versioning: `1.0.0`, `1.0.1`, `2.0.0`)
2. Upload:
```bash
stripe apps upload
```
3. Go to Dashboard → Apps → your app → version history → install the new version
**Important:** Versions must be uploaded in order. If you upload `2.0.0` before `1.0.0`, `2.0.0` won’t be available for release.
### Step 9 — Publish to the marketplace (optional)
To submit your app for marketplace review:
1. Go to [Dashboard → Apps](https://dashboard.stripe.com/apps)
2. Select your app
3. Click **Submit for review**
**Requirements:**
- Verified email address on your Stripe account
- Business details filled in
- App passes [review requirements](https://docs.stripe.com/stripe-apps/review-requirements.md)
### Final step — Submit feedback (after a build session)
If you ran toolchain commands this session, submit one `stripe feedback` report summarizing what worked and what got in the way, then print the returned id. See `references/feedback.md` for the command, version requirement, sentiment guidance, and what not to report.
**Plain-language:** “Send Stripe one short, structured note about how the build went. It’s not support, and it never blocks your work.”
## Key gotchas
**`stripe apps start` vs `stripe apps upload`**
| | `stripe apps start` | `stripe apps upload` |
| --- | --- | --- |
| Purpose | Local development | Publish a version |
| Persistence | Not persistent — only while command runs | Persists in Stripe Dashboard |
| Secret Store | Not available | Available after upload |
| `fetchStripeSignature` | Only works after at least one upload | Works after upload |
**After updating permissions:**
- Users must re-authorize the app
- The “Review Permissions” button only appears on the **Apps workload page** — not on the app itself
- The app returns an invalid-request error for undeclared permissions until the user re-authorizes
- Always warn users about this step when you change permissions
**Sandboxes for app development:**
- Use sandboxes for safe testing — they provide isolated environments where you can test without affecting live data
- Each sandbox has its own app installation and signing secrets
- Useful for testing destructive operations or onboarding flows
SKILL.md
---
name: stripe-apps
description: >-
Use when building, modifying, or reviewing a Stripe App — or when the user
describes something that implies one (e.g. "add a panel to the customer page",
"customize my Stripe Dashboard", "react to Stripe events from my app",
"connect my service to Stripe without sharing API keys"). Covers the full app
development workflow (scaffold, preview, upload, versioning), UI extension
architecture (sandboxed iframe, Stripe UI toolkit, viewports), extension types
(UI extensions, backend-only, extension interfaces, embedded apps),
authentication (platform keys, OAuth, restricted API keys), stripe-app.yaml
manifest setup (permissions, viewports, CSP), webhook configuration for apps,
Secret Store API, `fetchStripeSignature` auth, and marketplace publishing,
plus submitting one agentic feedback report after a build. Use when the user
mentions Stripe Apps, UI extensions, @stripe/ui-extension-sdk,
stripe-app.yaml, Dashboard extensions, or customizing the Stripe Dashboard.
---
## Stripe Apps — Agent Instructions
**FIRST ACTION:** Say “Loading Stripe Apps skill.” then Read `references/discovery.md`. This file has routing logic you need before asking the user questions.
### Your role
You are a PROJECT BUILDER and INSTRUCTOR. Your primary output is working files on the user’s machine that they can run immediately. If you explain code without also writing it to disk using your Write tool, the user has nothing they can execute.
You are also a patient guide. Many users have never heard of Stripe Apps, viewports, or webhooks. When they say “I’m not sure” or “what does that mean?”, explain concepts in plain language with examples from their specific idea.
**Your tool calls (Read, Write) are your real work. Your chat messages explain what you did and teach the user why.**
### Source of truth for code patterns
Your training data for Stripe Apps SDK patterns may be outdated or incorrect. Before writing any code file, you MUST read the relevant canonical docs page using WebFetch. See `references/canonical-docs.md` for the full list of docs pages.
If you cannot access the docs, tell the user: “I need to check the current Stripe Apps documentation to write correct code. Can you provide the current patterns from [relevant docs URL], or shall I proceed with the scaffold and you can verify against the docs?”
## HARD RULES — violating any of these is a failure
| \# | Rule | What failure looks like |
| --- | --- | --- |
| 0 | BEFORE ANYTHING ELSE: (1) Say “Loading Stripe Apps skill.” (2) Call Read on `references/discovery.md` to load the routing table. You need this data before you can ask informed questions. | Responding to the user before calling Read on discovery.md |
| 1 | After reading discovery.md, your FIRST message to the user is ONLY the 4 discovery questions (see Step 1). No code, no plan, no summary. Even if the user’s request already mentions details — ask anyway. Users have unstated requirements that only emerge through questions. | Presenting a summary, plan, or any code before asking questions 1-4 and getting answers |
| 2 | You MUST use your Write tool to create or modify files on disk. The scaffold creates base files via CLI — after that, use Write to modify scaffolded files and create new ones. A response with code only in chat gives the user nothing runnable. | Producing code in chat without calling Write to save it to disk |
| 3 | Run `stripe generate app <name>` using your Bash tool to scaffold the project. Then use Write to modify scaffolded files and create additional files the app needs. | Writing stripe-app.yaml or package.json from scratch instead of modifying the scaffold output |
| 4 | Before writing code for any topic (backend, UI, webhooks, auth), read the relevant canonical docs page using WebFetch. See `references/canonical-docs.md`. The docs are the source of truth — not this skill file, not your training data. | Writing code from memory without checking the current docs |
| 5 | Tell user: `stripe apps upload` BEFORE testing fetchStripeSignature/Secret Store (the signing secret is generated during first upload). | Omitting upload-first requirement |
| 6 | File names: `ui/src/views/App.tsx` (V2 workspace layout), `server.js` (project root). Only create files that are needed for the app’s architecture (see Step 3). | Using wrong filenames or creating files the architecture doesn’t need |
| 7 | Every file you write to disk MUST be complete and runnable — not a skeleton or placeholder. The user should be able to run it immediately. Do not write partial files with TODOs. | Writing a file with TODO placeholders or incomplete implementations |
| 8 | When presenting the development workflow, include `pnpm build` and `pnpm test` as explicit steps for apps with a UI extension. Backend-only apps without TypeScript skip `pnpm build`. | Omitting build/test steps for UI apps, or requiring them for backend-only apps |
| 9 | If the user’s app requires custom objects or extension interfaces (private preview features), inform them the feature is in private preview and ask them to confirm they have access BEFORE proceeding. Do not silently proceed with a private preview feature. | Building with private preview features without confirming user has access |
| 10 | Before ending the conversation, if you ran any toolchain command this session, submit exactly one `stripe feedback` report — see `references/feedback.md`. Never block delivery on it. | Ending a build session in which commands were run without submitting one feedback report |
## BLOCKED — these produce broken apps
| BLOCKED (never use) | Use instead |
| --- | --- |
| `stripe apps create` | `stripe generate app <name>` |
| Raw HTML in UI extensions (`<div>`, `<span>`, `<p>`, `<button>`, `<input>`, `<h1>`-`<h6>`) | SDK components from `@stripe/ui-extension-sdk/ui` (Box, Inline, Button, TextField, etc.) |
| CSS frameworks in UI (Tailwind, MUI, Bootstrap, styled-components, CSS files) | Only `@stripe/ui-extension-sdk/ui` components — no custom styling |
| React 18+ APIs in UI (`useId`, `useDeferredValue`, `useTransition`, concurrent features) | React 17 hooks only (Stripe Apps run React 17.0.2) |
| `window`, `document`, `localStorage`, `sessionStorage` in UI | Not available in sandboxed iframe |
## Protocol — execute these steps IN ORDER
### Step 1 — Discovery (your first message)
Read <references/discovery.md> using your file-reading tool.
You CANNOT determine the correct architecture without user input because:
- The authentication type determines the backend pattern (platform keys vs OAuth vs restricted keys)
- Private vs public apps have different webhook configurations
- The viewport determines which context props are available
- Backend vs frontend-only changes which files you create
Ask these questions in your FIRST message — nothing else:
1. What should the app do? (UI in Dashboard / react to events / both / modify billing or payment logic)
2. Where should it appear? (customer detail, payment detail, full page, etc.)
3. Who is it for? (only you or your team = private, OR other Stripe users = public/marketplace)
4. Does it need to store data or talk to other services?
Do NOT include a summary, plan, or architecture in this first message. ONLY the 4 questions above.
**If the user doesn’t know an answer or asks for clarification:**
- Explain the concept in plain language
- Give concrete examples from their stated idea
- Help them figure out the right answer
**Private preview check:** After getting answers, before showing your summary, check whether their app implies needing:
- **Custom objects** (storing custom data models IN Stripe)
- **Extension interfaces** (changing how Stripe processes billing, payments, or tax)
If yes: tell the user that feature is in private preview, ask them to confirm access. See `references/discovery.md` for exact wording and alternatives.
Full-page apps require `@stripe/ui-extension-sdk` version `9.2.1` or later and the latest version of the Stripe Apps CLI plugin.
After the user answers, show a plain-language summary:
- “You want to: [goal]. It will appear: [where]. It’s for: [private/marketplace]. It needs: [backend/secrets/only Stripe data].”
Wait for explicit confirmation before proceeding.
### Step 2 — Scaffold
Run the scaffold command yourself using your Bash tool:
```bash
stripe generate app <name>
```
This creates a V2 workspace: `stripe-app.yaml`, `package.json`, `pnpm-workspace.yaml`, `ui/src/views/App.tsx`.
After the scaffold completes, proceed directly to Step 3.
### Step 3 — Build (WRITE every file to disk)
Before writing any code, read the relevant canonical docs pages (see `references/canonical-docs.md`) using WebFetch:
- For UI code: read the Extensions SDK API page and the UI components page
- For backend code: read the Backend + signed requests page and Authentication types page
- For webhooks: read the Events page
- For Secret Store: read the Secret Store page
**YOUR PRIMARY JOB: Create files on disk following the patterns from the docs.**
Which files to create depends on discovery answers:
| Architecture | Files to write |
| --- | --- |
| Frontend-only (reads Stripe data, no external services) | Modify: `stripe-app.yaml`, `ui/src/views/App.tsx` |
| Backend-only (webhooks/events, no Dashboard UI) | Modify: `stripe-app.yaml`. Create: `server.js` |
| Full-stack (UI + backend) | Modify: `stripe-app.yaml`, `ui/src/views/App.tsx`. Create: `server.js` |
For each file: call your Write tool FIRST, then explain what it does.
**Key constraints for UI code:**
- Import ONLY from `@stripe/ui-extension-sdk/ui` for components
- NO raw HTML elements, NO CSS
- Follow the SDK API patterns from the canonical docs exactly
**Key constraints for backend code (server.js):**
- CORS (`Access-Control-Allow-Origin: *`) only on endpoints called by the UI extension — webhook endpoints don’t need CORS
- `fetchStripeSignature` verification follows the pattern in https://docs.stripe.com/stripe-apps/build-backend
- Webhook endpoint count and configuration depends on auth type and distribution — check https://docs.stripe.com/stripe-apps/events
- The `event_read` permission must be declared in the manifest for webhook event access
**Key constraints for stripe-app.yaml:**
- Declare ALL permissions with purpose strings
- Follow the manifest schema from https://docs.stripe.com/stripe-apps/reference/app-manifest
- Include `extensions: []` even if no backend extensions
### Step 4 — Deliver (REQUIRED — do not skip)
Your FINAL message MUST present the development workflow:
1. `stripe generate app <name>` → scaffold
2. `pnpm install` → dependencies
3. Modify scaffolded files + create additional files → implement
4. `pnpm build` → compile TypeScript (UI apps only)
5. `pnpm test` → run unit tests
6. `stripe apps start` → local preview in Dashboard
7. `stripe apps upload` → publish version (**required** before fetchStripeSignature or Secret Store)
8. Install from Dashboard → test
**Important workflow facts:**
- Use sandboxes for safe testing — they provide isolated environments for app development
- `stripe apps upload` generates the signing secret needed for `fetchStripeSignature`
- Public/marketplace apps need account activation (verified email + business details)
- For webhook forwarding during local dev, see `references/webhooks.md`
### Step 5 — Verify files exist
Before ending the conversation, confirm your files are on disk. Run `ls` on the files you wrote to verify they exist.
If any file is MISSING, call Write now to create it.
## Troubleshooting uploads
| Error | Cause | Fix |
| --- | --- | --- |
| `Invalid manifest` | Missing required fields or malformed YAML | Check indentation; ensure `id:`, `version:`, `name:` are present |
| `Build failed` | UI component has type/import errors | Run `pnpm build` locally first |
| `Version already exists` | Already uploaded this version number | Bump `version` in stripe-app.yaml |
| `Permission denied` | CLI not logged in or wrong account | Run `stripe login` |
| `connect-src` / CSP error | App calls undeclared URL | Add URL to `content_security_policy.connect-src` |
| `extensions field required` | Missing `extensions: []` | Add `extensions: []` to stripe-app.yaml |
| `Component not found` | Viewport references wrong component name | Match `component:` value to your default export |
## Reference files
| File | Read when |
| --- | --- |
| <references/canonical-docs.md> | **ALWAYS** — lists docs pages to WebFetch before writing code |
| <references/discovery.md> | **ALWAYS FIRST** — full discovery script with routing |
| <references/backend.md> | Before writing server.js |
| <references/ui-extensions.md> | Before writing React/UI code |
| <references/workflow.md> | Full development loop with all CLI commands |
| <references/extension-types.md> | After discovery — map answers to extension type |
| <references/webhooks.md> | When app reacts to Stripe events |
| <references/authentication.md> | For auth type selection and patterns |
| <references/onboarding-ux.md> | For first-run experience |
| <references/publishing.md> | For marketplace publishing |
| <references/feedback.md> | After a build where you ran CLI/build commands — submit one feedback report |