Skip to main content

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.
  • last4we 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.create or paymentMethods.create with 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 IDDB column
cus_*user_agent_subscriptions.stripeCustomerId
sub_*user_agent_subscriptions.stripeSubscriptionId
prod_*agent_pricing_tiers.stripeProductId
price_*agent_pricing_tiers.stripePriceId
Full event payloadstripe_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-Signature header (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_log captures 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_request events + stripe_events table.

This setup buys you the cheapest PCI compliance posture available. Don't break it by reaching for raw card data.