> ## Documentation Index
> Fetch the complete documentation index at: https://docs.r28.ai/charter/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Charter works for any HTTP API, including the user's own. The packs are examples of the result, not the limit: an API with no pack is declared the same way, following https://raw.githubusercontent.com/r28ai/charter/main/docs/.mintlify/skills/declaring-charter-tools/SKILL.md.
> Charter is for agents that act in customers' accounts: every call is checked field by field before it's sent, the app says whose account each call is for, and the same declarations list what each tool can reach. https://docs.r28.ai/charter/llms.txt says what it is and lists every page.

# Register a Stripe App

> The Stripe App your users install to connect their Stripe accounts: the manifest, the upload, the test link, and why the test needs two accounts.

Before your users can connect their Stripe accounts by OAuth, Stripe needs a
[Stripe App](https://docs.stripe.com/stripe-apps/api-authentication/oauth)
that you own and they install. There's no form to fill in. A Stripe App is a
small project you generate with the Stripe CLI, describe in a manifest and
upload. It takes about ten minutes, most of them installing the CLI.

Only connecting your own account? A secret or restricted key is all the
[`stripe`](https://docs.r28.ai/charter/charter/packs/stripe) pack needs: [Set up Stripe](https://docs.r28.ai/charter/charter/auth/setup/stripe.md).
Users who would rather paste a restricted key need no app either:
[Stripe OAuth](https://docs.r28.ai/charter/charter/auth/providers/stripe.md#a-pasted-restricted-key).

> [!NOTE]
>
> The test takes two Stripe accounts. Upload the app from one whose business is
> verified: a sandbox, or an account that hasn't submitted its business details,
> gets no test links. Then install it from a different account. Stripe issues a
> token for the account that owns the app, and refuses it on every call.

Every Stripe link below opens in a new tab, so this page stays where you left it.

**1. Install the CLI**

```bash theme={null}
brew install stripe/stripe-cli/stripe
stripe plugin install apps
stripe plugin install generate
stripe login
```

Stripe Apps also needs Node.js 22 or later and pnpm 10. `stripe login` pairs
the CLI with one account. Pick the verified account that will own the app.

The first command that touches an app asks the account to accept the Stripe
Apps Agreement, and prints the Dashboard link to do it.

**2. Generate the app**

```bash theme={null}
stripe generate app example-app --id example-app --display-name "Example App" --non-interactive
cd example-app
stripe apps remove view FullpageView --non-interactive
rm ui/src/views/FullpageView.tsx ui/src/views/FullpageView.test.tsx
```

The display name is what your users see when they install. The ID can't be
changed after the first upload, and it's unique across all of Stripe,
sandboxes included, so an upload anywhere claims it.

The generated project is a Dashboard extension with a page of its own. An app
that only connects accounts needs no page, so the last two lines remove it.

**3. Declare OAuth and the permissions**

```bash theme={null}
stripe apps set distribution public
stripe apps set api-access-type oauth
stripe apps grant permission customer_read "Look up customers"
stripe apps grant permission charge_read "Read charges and refunds"
stripe apps grant permission invoice_read "Read invoices"
```

Each permission is a line on the consent screen, with its purpose beside it,
and the token can do nothing else. A tool that needs one the app lacks fails
with a `403` coded `more_permissions_required_for_application`. Grant what the
pack's tools need, from
[Stripe's permissions reference](https://docs.stripe.com/stripe-apps/reference/permissions):

| Tools | Read | Write |
| - | - | - |
| `customers_*` | `customer_read` | `customer_write` |
| `charges_*`, `refunds_*` | `charge_read` | `charge_write` |
| `invoices_*`, `invoice_items_*` | `invoice_read` | `invoice_write` |
| `products_*` | `product_read` | `product_write` |
| `prices_*` | `plan_read` | `plan_write` |
| `payment_intents_*` | `payment_intent_read` | `payment_intent_write` |
| `payment_methods_*`, `customer_payment_method*` | `payment_method_read` | `payment_method_write` |
| `subscriptions_*` | `subscription_read` | `subscription_write` |
| `checkout_sessions_*` | `checkout_session_read` | `checkout_session_write` |
| `disputes_*` | `dispute_read` | `dispute_write` |
| `balance_retrieve` | `balance_read` | |
| `balance_transactions_list` | `balance_transaction_source_read` | |
| `payouts_*` | `payout_read` | |
| `transfers_*` | `transfer_read` | |
| `application_fees_list` | `application_fee_read` | |
| `accounts_*` | `connected_account_read` | |

Prices are `plan_read` and `plan_write`, not `price_*`.

No command sets the redirect URLs. Add them to `stripe-app.yaml`, under
`declarations`:

```yaml stripe-app.yaml theme={null}
declarations:
    distribution_type: public
    stripe_api_access_type: oauth
    allowed_redirect_uris:
        - https://app.example.com/oauth/stripe/callback
        - http://localhost:8765/callback
    stripe_api_access:
        permissions:
            - permission: customer_read
              purpose: Look up customers
```

The first URL is the default. Live mode redirects only to `https`. A test-mode
install may come back to `http://localhost`, which is how a script on your
machine catches the code. Remove it before you submit the app for review:
[Stripe's publishing guide](https://docs.stripe.com/stripe-apps/publish-app)
lists a localhost or placeholder redirect URL among the common reasons an app
is turned back.

**4. Upload it**

```bash theme={null}
stripe apps upload --force
```

Without `--force`, the CLI stops at a check that calls an app with no page
"no functionality yet". Here the permissions are the function.

Upload from the account's test mode. If the CLI answers "You're in live mode",
`stripe switch context` moves it out.

**5. Turn on external testing**

In the Dashboard, open **Apps overview**, then **Created**, and click the app.
Under **External testing**, click **Get started**, keep the version you just
uploaded, and click **Set version**.

The page now has a **Test OAuth** section with three links: **Live mode link**,
**Test mode link** and **Sandbox link**. Clicking one copies it. Copy the
**Test mode link**:

```
https://marketplace.stripe.com/oauth/v2/chnlink_.../authorize?client_id=ca_...&redirect_uri=...
```

The **Install links** row higher up has the same three names. Those are the
public links. They stay inactive until the app is published, and they are the
ones App Review asks for.

**6. Point the flow at the test link**

The link's `client_id` is the app's client ID. Its path, up to `authorize`, is
the consent endpoint until you publish. After that it's the one in
`STRIPE_APPS`, the constant on [Stripe's provider page](https://docs.r28.ai/charter/charter/auth/providers/stripe.md).

```python stripe_test_link.py theme={null}
import dataclasses

from charter.auth import OAuth2Flow

# Until the app is published, Stripe honours only the External test link,
# whose path names a channel. The query string is the flow's to build.
STRIPE_APPS_TEST = dataclasses.replace(
    STRIPE_APPS,
    authorization_endpoint="https://marketplace.stripe.com/oauth/v2/chnlink_.../authorize",
)

flow = OAuth2Flow(
    STRIPE_APPS_TEST,
    client_id=os.environ["STRIPE_APP_CLIENT_ID"],  # ca_..., from the link
    client_secret=os.environ["STRIPE_SECRET_KEY"],  # sk_test_..., of the account that owns the app
    redirect_uri="http://localhost:8765/callback",
)
request = flow.authorize()
```

The secret key's mode follows the link's. A test-mode install is exchanged with
the owner's `sk_test_` key, and a live one with its `sk_live_` key.

**7. Install it from another account**

Open the link `flow.authorize()` built. Stripe says the app is a test version
and that installing it into test mode won't affect the live account. Click
**Continue**, choose an account **other than the one that owns the app**, and
approve the permissions.

The callback receives `code` and `state`, and Stripe adds `stripe_user_id`: the
account that installed. Exchange the code as on
[Getting the grant](https://docs.r28.ai/charter/charter/auth/oauth-flow.md). The access token begins `rk_test_`, and
the refresh token rolls on every use. From here the per-user client is the one
in the [Stripe provider page](https://docs.r28.ai/charter/charter/auth/providers/stripe.md).

[`scripts/live_oauth_check.py`](https://github.com/r28ai/charter/blob/main/scripts/live_oauth_check.py)
runs this step from a terminal against your app, then checks the result: the
pack's read tools, a refresh, and two workers taking turns on one grant.

## Publish it

Until Stripe approves the app, only the accounts that open its External test
link can connect, 25 at most. Publishing opens it to every Stripe account,
through the endpoint `STRIPE_APPS` already declares. Before approval that
public link answers "The provided OAuth link is invalid".
[Stripe's publishing guide](https://docs.stripe.com/stripe-apps/publish-app)
has the full list. For an app like this one it comes to:

* **An activated account.** Stripe needs your business details first, and
  publishes one app per account.
* **Your real callback.** Replace the `localhost` redirect with your `https`
  one, raise `version` in `stripe-app.yaml`, upload, and set that version as
  the one to publish.
* **An icon.** `icon` in `stripe-app.yaml`, the same square image as the
  listing's logo, at least 300 by 300 pixels.
* **The listing.** A name of 35 characters or fewer that avoids "Stripe",
  "app", "Generator", "free" and "paid"; a subtitle; up to three feature images
  that show no real customer data; pricing; a privacy policy; and one or two
  support channels.
* **What the reviewers need.** Testing steps, and credentials for test accounts
  they can sign in to without two-factor authentication. The install URL you
  submit must use the public link from the app's **Settings** tab, not the
  External test link.

Stripe answers within about four business days. A later change to the app or
its listing is reviewed again.

Once it's published, drop `STRIPE_APPS_TEST` and build the flow on
`STRIPE_APPS` as declared. A live-mode install is exchanged with the owner's
`sk_live_` key. To check the published link end to end, with the address your
browser lands on pasted back in:

```bash theme={null}
export STRIPE_SECRET_KEY=sk_live_...
uv run python scripts/live_oauth_check.py connect --provider stripe \
  --link 'https://marketplace.stripe.com/oauth/v2/authorize?client_id=ca_...&redirect_uri=https://app.example.com/oauth/stripe/callback'
uv run python scripts/live_oauth_check.py check --provider stripe
```

## When it does not work

> **Verify your business to get started with external testing**
>
> The app was uploaded from a sandbox, or from an account that hasn't
> submitted its business details. Its app page shows no test links. Upload it
> again from a verified account, under a new ID.

> **Public apps uploaded from a sandbox account must set sandbox\_install\_compatible to true**
>
> The CLI is logged into a sandbox. Setting the flag lets the upload through,
> but the sandbox still asks for business verification before it shows test
> links. Log the CLI into the verified account instead.

> **App id already taken**
>
> App IDs are unique across Stripe, and an upload from any account, a sandbox
> included, claims one. Change `id` in `stripe-app.yaml` and upload again.

> **The provided OAuth link is invalid**
>
> That is the public link, `/oauth/v2/authorize`, before the app is
> published. Until Stripe approves it, connect through the External test
> link.

> **Installed from the Dashboard, and no code arrived**
>
> The **Install** button on the app's own page installs it on that account
> without the consent flow, so nothing is redirected. Install from the test
> link, from another account.

> **403 on every call: does not have access to account**
>
> The message ends "Application access may have been revoked", but the app
> is still installed. It was installed on the account that owns it. Install
> it from another account.

> **403 more\_permissions\_required\_for\_application**
>
> The tool needs a permission the manifest doesn't grant. Grant it, raise
> `version` in `stripe-app.yaml`, upload, and set the new version under
> **External testing**.

> **invalid\_grant: Refresh token does not exist**
>
> The refresh token was used and its replacement was never saved. Stripe
> rolls refresh tokens on every use, so the old one stops working at once.
> Store the new one in `on_refresh`, then have the user connect again.

## Related

* [Stripe OAuth](https://docs.r28.ai/charter/charter/auth/providers/stripe.md) — the server constant, the per-user client, and a pasted restricted key beside OAuth
* [Getting the grant](https://docs.r28.ai/charter/charter/auth/oauth-flow.md) — the two routes, `state` and PKCE, inside your app
* [Stripe](https://docs.r28.ai/charter/charter/packs/stripe.md) — the pack, form encoding, idempotency and Connect


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.