services/stripe-app/README.md
A Stripe App that surfaces PostHog product analytics data directly inside the Stripe Dashboard.
Connecting PostHog to Stripe requires two OAuth exchanges in sequence:
Stripe OAuth (Stripe → PostHog):
the user clicks "Connect" in PostHog,
which redirects them to marketplace.stripe.com/oauth/v2/authorize.
Stripe asks the user to grant PostHog access to their Stripe account.
On success Stripe redirects back to PostHog with an authorization code
that PostHog exchanges for a Stripe access token.
This token is stored in the PostHog Integration model (sensitive_config.access_token)
and is used server-side for data imports and for writing secrets to Stripe.
PostHog OAuth (PostHog → Stripe Secret Store):
immediately after the Stripe token exchange,
the PostHog backend creates a PostHog OAuth access + refresh token pair
(scoped to the team) and writes them — along with the region (us/eu) —
into Stripe's Secret Store via the Stripe API.
The Stripe App reads these secrets at runtime to call PostHog APIs.
The three secrets stored in Stripe are:
| Secret name | Value |
|---|---|
posthog_region | us or eu |
posthog_access_token | PostHog OAuth token |
posthog_refresh_token | PostHog OAuth refresh |
Key files:
posthog/models/integration.py — StripeIntegration class: writes and clears secrets in Stripeposthog/api/integration.py — triggers write_posthog_secrets() after Stripe OAuth callbackposthog/settings/integrations.py — env vars (STRIPE_APP_CLIENT_ID, STRIPE_APP_SECRET_KEY,
STRIPE_POSTHOG_OAUTH_CLIENT_ID, STRIPE_APP_OVERRIDE_AUTHORIZE_URL)src/posthog/auth.ts — reads/writes/clears credentials in Stripe's Secret Storesrc/posthog/client.ts — authenticated HTTP client for PostHog APIssrc/components/PostHogConnect.tsx — connection status UI + dev-mode token entrysrc/views/ — Stripe Dashboard view entry points (Home, Settings, Onboarding)src/constants.ts — typed access to manifest constants (PostHog URLs)stripe-app.json — production manifest: PostHog URLs, permissions, CSPstripe-app.dev.json — extends the production manifest, overrides URLs to localhost:8010stripe login)localhost:8010You can configure mprocs to include the stripe app in your configuration when running hogli dev:setup. If you don't wanna change your mprocs setup, however, you can run it manually
# Via hogli (recommended — also starts via mprocs)
hogli start:stripe:app
# Or directly
pnpm --filter=@posthog/stripe dev
This runs stripe apps start --manifest stripe-app.dev.json,
which serves the app UI inside the Stripe Dashboard in test mode.
There are two approaches depending on whether you need to test the OAuth flow itself.
The dev server automatically generates PostHog OAuth tokens on startup
(via manage.py generate_stripe_app_tokens).
You'll see the tokens printed in the logs when the server starts — no manual command needed.
The tokens are reused across restarts as long as they haven't expired (24 hours).
If you need fresh ones, run the command manually with --force:
python manage.py generate_stripe_app_tokens --team-id=1 --force
On first run, if STRIPE_POSTHOG_OAUTH_CLIENT_ID is not set,
the command will automatically create an OAuthApplication
and write its client_id to your .env.
To connect the app:
stripe-app pane).stripe apps start).
The "Not connected" screen shows a Dev mode section at the bottom
with fields for region, access token, and refresh token.The app stores them in Stripe's Secret Store just like the production flow would, and the connection is established.
By default the startup script uses team ID 1.
Pass a different ID as an argument: bin/start-stripe-app 42.
If you need to test the actual double OAuth exchange end-to-end,
you need to work around the fact that Stripe's OAuth redirect cannot target localhost.
The trick is to temporarily route through a production URL and then redirect back locally.
Set up environment variables in your .env:
STRIPE_APP_CLIENT_ID=ca_... # from Stripe Apps dashboard
STRIPE_APP_SECRET_KEY=sk_test_... # from Stripe API keys
STRIPE_POSTHOG_OAUTH_CLIENT_ID=... # auto-created by the management command, or create manually
STRIPE_APP_OVERRIDE_AUTHORIZE_URL=https://marketplace.stripe.com/oauth/v2/chnlink_.../authorize
The STRIPE_APP_OVERRIDE_AUTHORIZE_URL must be a channel link URL
(not the standard marketplace URL) because channel links allow
non-published app versions to be installed.
Start the OAuth flow from PostHog (localhost:8010). PostHog redirects to Stripe's authorize page.
Stripe redirects back to the redirect_uri — which is derived from SITE_URL
and will be https://localhost:8010/integrations/stripe/callback
(or https://us.posthog.com/... if that's your SITE_URL).
Since Stripe requires HTTPS redirect URIs,
and localhost doesn't have a valid HTTPS certificate,
you have two options:
a. Temporarily set SITE_URL to a production URL (e.g. https://us.posthog.com),
start the OAuth flow, then once Stripe redirects to that production URL,
copy the callback URL from the browser, change the host to localhost:8010,
and paste it into your browser to complete the flow locally.
b. Use a tunnel (e.g. ngrok, Cloudflare Tunnel) to expose your local PostHog
on a public HTTPS URL and set SITE_URL to that URL.
PostHog completes the exchange: exchanges the authorization code for a Stripe access token, creates PostHog OAuth tokens, and writes all three secrets to Stripe's Secret Store.
The full approach is more involved but tests the entire production flow. For day-to-day development, the quick approach is recommended.
cd services/stripe-app
pnpm run upload
This creates a package-lock.json (required by Stripe) and uploads the app.
| Variable | Description |
|---|---|
STRIPE_APP_CLIENT_ID | Stripe App OAuth client ID (from Stripe Apps dashboard) |
STRIPE_APP_SECRET_KEY | Stripe API secret key for token exchange |
STRIPE_POSTHOG_OAUTH_CLIENT_ID | Client ID of the PostHog OAuthApplication used by the Stripe App |
STRIPE_APP_OVERRIDE_AUTHORIZE_URL | Channel link authorize URL (required for non-published app installations) |