Files
member-console/docs/stripe.md
T

9.5 KiB

title, audience, summary
title audience summary
Stripe Integration
admin
developer
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