Skip to main content
A Stripe App gives the link-and-redirect experience. Your user installs it from the link, picks an account, and comes back. The app’s permissions are set in its manifest, not in the link, so the flow sends no scopes. Stripe’s token endpoint takes your app’s own secret key as the credential: passed as client_secret, it is sent the way curl -u sk_live_...: sends it. Register a Stripe App builds the app and tests it.

The server

stripe_server.py
secret_key_basic is the secret key as Basic auth. default_expires_in is the lifetime Stripe’s token response leaves out: Refresh behaviour. revocation uninstalls the app, which is how Stripe ends a grant: Disconnecting.

Connecting a user

stripe_users.py

Before you publish

Until the app is published, that consent link is not active. Stripe honours only the link on the app’s External test tab, whose path names a channel: /oauth/v2/chnlink_.../authorize. Point the flow at that path while you test, and install from an account other than the one that owns the app. Stripe issues a token for the owner’s own account and then refuses it on every call.

Refresh behaviour

The access token is a restricted key, rk_live_... or rk_test_..., limited to the permissions in the app’s manifest, and it goes out as Bearer like any other. Stripe access tokens last an hour, and its token response doesn’t say so. The constant declares it with default_expires_in, so the client renews each token shortly before the hour instead of learning from a refused call. Stripe’s refresh tokens roll on every use, and a refresh revokes the previous access token within seconds. So on_refresh stores the new access token, its expiry and the refresh token, and stored_grant reads them back: every process uses the token one refresh produced instead of refreshing on its own. Processes that find the token due at the same moment still refresh together, and each refresh revokes the token of the one before. Serving from more than one process, pass refresh_lock as well: A lock around the refresh has the code and what it measured without one.

A pasted restricted key

Not every product wants to publish a Stripe App. The other path is a restricted key (rk_...): the user creates it with only the permissions your tools use and pastes it, and stripe_for returns StaticTokenProvider(row.api_key). Many tools start this way. Each tool declares the permission Stripe’s reference names for its endpoint, as the stripe.RESTRICTED_KEY kind. “Write permissions imply read permissions”, so a reader takes either:
stripe_restricted_key.py
That list is what to set to Read or Write when the key is made; everything else stays None. A secret key needs nothing. A call the key may not make raises InsufficientScope with the permission Stripe names (“Having the ‘rak_charge_read’ permission would allow this request to continue”). Seven endpoints are missing from Stripe’s table and take their resource’s permission, as every neighbouring endpoint does. Stripe’s own refusals confirmed six of them, and 62 of the pack’s tools in all, against a test-mode key holding no permission the pack uses (scripts/live_stripe_permissions_check.py). Creating a refund is the one that cannot be asked this way: Stripe looks the charge up first, and tells a key that cannot read charges that the charge does not exist. A Connect platform is different again. It acts on its connected accounts with its own key and a Stripe-Account header, which is per call, not a credential.

Disconnecting

Stripe has no endpoint that revokes a token: a grant is an install of your app, and it ends when the install does. Stripe’s App Installs API uninstalls it, and StripeAppUninstall is that, declared as the revocation. It looks the install up for the stripe_user_id Stripe’s token response carries, then uninstalls it, with your secret key. The client keeps the account from the exchange or any refresh, and refreshes once first if it has seen neither, so nothing extra needs storing. The call is the same as for every other provider:
stripe_disconnect.py
Measured, the uninstall answered 200, the install was gone within about fifteen seconds, the access and refresh tokens were refused, Stripe sent apps.install.deleted, and the next install from your link was a fresh one. An account without the app returns False; a wrong app ID or another account’s key raises. revoke() returns once Stripe accepts the uninstall (status: uninstalling), not once it is done. Charter stops using the grant at once; Stripe finishes seconds later, 10 to 15 measured, and until then the old access token still works. Treat the disconnect the way Stripe’s own states suggest: delete your row, show the user “Disconnecting”, and offer Reconnect when apps.install.deleted arrives, which your Connect webhook endpoint receives with account set to theirs. Reconnecting before then can land on the install still being removed. A script or a test that must not go on until the install is gone declares StripeAppUninstall(app="app_...", wait_seconds=30) instead; still uninstalling at the deadline is logged, and the call still returns True. It waits holding the client’s refresh lock, which is one more reason to keep it out of a server. Not Connect’s oauth/deauthorize: Stripe documents it for Standard accounts only, and measured against an app’s grant it ended the access but left the app installed, so your install link answered “already installed” and issued tokens Stripe refused until the user uninstalled the app from their Dashboard. A pasted restricted key was made by the user, and only they can delete it, in their Dashboard. Delete your row.

Where the rest is