Stripe & PCI scope
The marketplace stays out of PCI scope by never touching card data. All sensitive payment information lives in Stripe.
How
- Stripe Checkout — the user is redirected to Stripe-hosted pages. Card details are entered on Stripe's domain.
- Stripe Webhooks — we receive event notifications, never card numbers.
- Stripe Customer Portal (if wired) — same: Stripe-hosted, no card data flows back.
The marketplace database stores only:
stripeCustomerId— opaque ID, not card data.stripeSubscriptionId— opaque ID.stripeProductId,stripePriceId— Stripe-side IDs.last4— we do not store this. Stripe's API can surface it on demand if you need to show it.
SAQ-A eligibility
Because we don't store, process, or transmit cardholder data (and the iframe / redirect approach means card fields never appear on our origin), we qualify for SAQ A — the lightest PCI questionnaire.
To stay SAQ-A:
- Never call Stripe's raw
tokens.createorpaymentMethods.createwith card numbers from our own UI. - Don't embed Stripe Elements on our pages without the appropriate iframe-only mode.
- Don't accept card data via support channels (email, chat).
What we do store
| Stripe-side ID | DB column |
|---|---|
cus_* | user_agent_subscriptions.stripeCustomerId |
sub_* | user_agent_subscriptions.stripeSubscriptionId |
prod_* | agent_pricing_tiers.stripeProductId |
price_* | agent_pricing_tiers.stripePriceId |
| Full event payload | stripe_events.payload JSONB |
The stripe_events.payload JSONB contains whatever Stripe sent. Stripe redacts card numbers before they leave their systems — webhook payloads include last4, brand, etc., but never the full PAN.
Webhook signature
stripe.webhooks.constructEvent(rawBody, signature, STRIPE_WEBHOOK_SECRET) rejects:
- Unsigned requests.
- Requests with a stale
Stripe-Signatureheader (5 minute tolerance). - Requests where the secret doesn't match.
This is the same level of authenticity as an mTLS link.
Compliance hygiene
- Never log full webhook payloads to PostHog. The
audit_logcaptures only metadata. - Never expose Stripe IDs in error messages to end-users.
- Don't store Stripe IDs in browser-side state longer than the active session.
What an audit would ask
- "Show us where card data is stored." → "It isn't. We use Stripe Checkout. Here's the network diagram."
- "Show how webhooks are authenticated." →
stripe-webhook.service.ts: verify(). - "Show how secrets are rotated." → Secrets management.
- "Show your audit trail." → PostHog
api_requestevents +stripe_eventstable.
This setup buys you the cheapest PCI compliance posture available. Don't break it by reaching for raw card data.