155 lines
9.5 KiB
Markdown
155 lines
9.5 KiB
Markdown
---
|
|
title: "Stripe Integration"
|
|
audience: [admin, developer]
|
|
summary: "Configure Stripe: flags and file-based secrets, the webhook endpoint and events, catalog-to-Stripe sync, and making a product purchasable."
|
|
---
|
|
|
|
# Stripe
|
|
|
|
This guide covers connecting the member console to Stripe for payment processing. The member console is the source of truth for the product catalog — Stripe handles payment execution.
|
|
|
|
## Prerequisites
|
|
|
|
- A Stripe account (test mode is fine for initial setup)
|
|
- The Stripe secret key (`sk_test_...` or `sk_live_...`)
|
|
- A webhook endpoint signing secret (`whsec_...`)
|
|
|
|
## Configuration
|
|
|
|
| Flag | Description |
|
|
|------|-------------|
|
|
| `--stripe-api-key` | Stripe secret key |
|
|
| `--stripe-api-key-file` | Path to file containing the API key |
|
|
| `--stripe-webhook-secret` | Webhook endpoint signing secret |
|
|
| `--stripe-webhook-secret-file` | Path to file containing the webhook secret |
|
|
|
|
All flags support `_FILE` variants for secret injection from mounted files.
|
|
|
|
There is no mode setting. The console derives test or live from the API
|
|
key's prefix at boot (`sk_live_` and `rk_live_` are live, `sk_test_` and
|
|
`rk_test_` are test); any other prefix fails boot. "View in Stripe" links
|
|
carry the mode and nothing else: Stripe opens them in the account and
|
|
context your dashboard session is in, and offers its own switch when the
|
|
object lives elsewhere. The console never reads or stores the account id:
|
|
reading it needs the Connect "Accounts Read" permission on a restricted
|
|
key, and a restricted key with only the permissions the console uses is
|
|
the recommended deployment.
|
|
|
|
Stripe's own `livemode` flag cannot narrow this further: a legacy test key
|
|
and every sandbox report `livemode: false` alike, so the flag separates
|
|
test from live but tells no sandbox apart from another sandbox or from
|
|
legacy test mode. Only reading a stored id back under the current key
|
|
shows that the object belongs to a different environment.
|
|
|
|
## Moving between environments
|
|
|
|
A deployment moves environments by changing its key: from a sandbox to
|
|
the live account when it goes into service, or back to a sandbox to
|
|
rehearse. Swap the API key and the webhook signing secret together and
|
|
restart; the two belong to one environment, and a secret left behind
|
|
keeps verifying the old environment's events.
|
|
|
|
Every synced object the console holds records the environment that made
|
|
it (`livemode` on each `stripe.*_mappings` row, written from the object
|
|
Stripe returned; rows from before the column exists hold nothing and
|
|
read as unverified). After the restart:
|
|
|
|
- The console compares a fingerprint of the key (a SHA-256 digest; the
|
|
key itself is never stored) with the one the last check ran under and,
|
|
when they differ, starts a background check that
|
|
reads every synced product and price back under the new key. The
|
|
Stripe integration page's **Environment check** section shows it
|
|
running, then its result (`Last checked ..., 34 verified, 2 stale.`),
|
|
and offers **Check now** to run it again. Nothing waits on the
|
|
check: boot finishes as before.
|
|
- A product whose mapping records the other environment shows it on its
|
|
Purchasability panel (`Synced in test, key is live`) and is not
|
|
purchasable until it is created again. The panel's sync control reads
|
|
**Create in live** (or **Create in test**) in that case and creates the
|
|
product and its price in the current environment; the old ids are
|
|
replaced.
|
|
- A mapping the check could not find under the new key (a move between
|
|
two sandboxes, which Stripe reports identically) is marked `stale` and
|
|
reads `Not found in live mode`; it behaves like a mapping from the
|
|
other environment.
|
|
- A member checkout, a subscription reconcile, a plan switch or a
|
|
scheduled change against such a mapping is refused before Stripe is
|
|
called and logged; nothing is sent under a key that cannot reach the
|
|
object.
|
|
- Webhook events from the other environment are stored as `refused`,
|
|
never processed, and counted on the Stripe integration page (`3 events
|
|
arrived in test mode under a live key.`). Unverified mappings block
|
|
nothing.
|
|
- The billing views show the rows recorded in the current environment.
|
|
When rows from the other environment exist, one line above the view
|
|
names how many are not shown and switches the view to all of them,
|
|
each marked with its environment.
|
|
|
|
Customers are not read back by the check: a customer id left behind by a
|
|
move between two sandboxes surfaces when that member's next checkout
|
|
fails, and the console then creates the customer again.
|
|
|
|
## Webhook endpoint
|
|
|
|
Register a webhook in the Stripe Dashboard pointing to:
|
|
|
|
```
|
|
https://<your-domain>/webhooks/stripe
|
|
```
|
|
|
|
Set the endpoint's API version to `2025-02-24.acacia`, the version pinned by `stripe-go/v81`; later versions (Basil and after) drop the top-level `invoice.subscription` and `invoice.payment_intent` fields the invoice handler reads.
|
|
|
|
The endpoint's signing secret verifies the events of the endpoint that issued it, so a valid signature proves that Stripe sent the event to this endpoint and nothing more: it does not prove the event came from the environment the current API key names.
|
|
|
|
Subscribe to these event types:
|
|
|
|
- `customer.created`, `customer.updated`, `customer.deleted`
|
|
- `product.created`, `product.updated`, `product.deleted`
|
|
- `price.created`, `price.updated`
|
|
- `checkout.session.completed`, `checkout.session.expired`
|
|
- `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`
|
|
- `invoice.created`, `invoice.finalized`, `invoice.paid`, `invoice.payment_failed`, `invoice.voided`
|
|
- `payment_method.attached`, `payment_method.detached`, `payment_method.updated`
|
|
|
|
## How it works
|
|
|
|
The member console is the catalog source of truth; Stripe holds a mirror used for payment execution. Catalog objects do **not** sync to Stripe automatically — syncing a product to Stripe is an **explicit operator action** (see "Making a product purchasable" below). Once synced, members subscribe via Stripe Checkout, Stripe runs the billing cycle, and webhooks project Stripe state (subscriptions, invoices, payments) back into the member console's core tables.
|
|
|
|
Sync runs through a transactional outbox: the operator action enqueues `create_stripe_product` and `create_stripe_price` entries, and a worker drains them against the Stripe API. The resulting `stripe.product_mappings` / `stripe.price_mappings` rows record the mapping and its `sync_status`, which moves from `pending` to `synced` (or to `deleted`, or to `stale` when the environment check cannot find the id under the current key). `dead_letter` is a status on the outbox entry itself, not on the mapping: when an entry fails repeatedly and the outbox marks it `dead_letter`, the mapping it was trying to create stays at `pending` until an operator retries the entry and the retry succeeds.
|
|
|
|
Every Stripe object the console reads carries a `livemode` field naming the environment it exists in, except `SubscriptionItem`, which has no such field: a subscription item's environment can only be taken from its parent subscription.
|
|
|
|
## Making a product purchasable
|
|
|
|
A member can only buy a product once **every** purchasability precondition is met. The operator product edit page (`/operator/products/{id}`) shows a **Purchasability** panel that evaluates each one and renders a single verdict — **Purchasable** or **Incomplete** — naming exactly what is missing. The preconditions:
|
|
|
|
1. **Published** — set the product's lifecycle to `published`.
|
|
2. **Public & active** — mark it Active and Public so it appears in the member catalog.
|
|
3. **Structural kind** — put it on a plan ladder to sell it as a plan (a single-tier ladder is the idiom for a standalone plan), **or** give it a product type (`addon` / `usage` / `one_time`). A published, untyped product on no ladder is invisible "limbo", which the panel flags.
|
|
4. **Active price** — add a price on the product's Prices view.
|
|
5. **Stripe-mapped price** — the active price is mapped to a live Stripe price.
|
|
|
|
The last step is the one to do **explicitly**: on the Purchasability panel, click **Sync to Stripe**. That enqueues the product and price sync; the precondition shows **Sync pending** until the outbox worker drains, then flips to **Met** and the verdict becomes **Purchasable**. Adding a price does **not** auto-sync — click Sync to Stripe whenever a product or its active price is not yet mapped. When the mapping records another environment, or the environment check marked it stale, the same control reads **Create in live** (or **Create in test**) and creates the product and price again in the current one ("Moving between environments" above).
|
|
|
|
If Stripe is **not configured** for the deployment (no API key), the panel says so on the Stripe-mapped precondition instead of a dead "missing" marker — pricing simply cannot be Stripe-mapped, though grant-based access still works without Stripe.
|
|
|
|
## Operator guidelines
|
|
|
|
### Do in the member console
|
|
|
|
- Create and manage products, prices, and entitlement sets
|
|
- Monitor sync status in the operator panel
|
|
- Create grants for admin-granted access (bypasses billing)
|
|
|
|
### Do not do in Stripe
|
|
|
|
- **Do not create products or prices in the Stripe Dashboard.** They will not be recognized by the member console.
|
|
- **Do not delete products or prices in Stripe.** Deactivate them in the member console instead.
|
|
- **Do not modify subscription quantities or prices in Stripe.** Changes should flow through the member console.
|
|
|
|
### Acceptable in Stripe
|
|
|
|
- View payment details, invoices, and customer records
|
|
- Handle disputes and issue refunds (until member console tooling exists)
|
|
- Configure tax settings, payment method types, and Stripe-level features the member console does not manage
|