Stripe Integration Guide

Technology: stripe · Category: payments · Last reviewed: 2026-08-23

Source: https://tech-stack.codeamanilabs.org/guide/stripe

Insight:

Stripe is codeAmani's default payment rail — the US-first customer base pays by card and wallet, and Stripe Checkout is the shortest path to production. M-Pesa (Daraja) is a separate rail used only for Kenya-targeted projects. Pin your apiVersion explicitly, verify every webhook signature, and pass an idempotency key on every state-changing create call.

███████╗████████╗██████╗ ██╗██████╗ ███████╗
██╔════╝╚══██╔══╝██╔══██╗██║██╔══██╗██╔════╝
███████╗   ██║   ██████╔╝██║██████╔╝█████╗
╚════██║   ██║   ██╔══██╗██║██╔═══╝ ██╔══╝
███████║   ██║   ██║  ██║██║██║     ███████╗
╚══════╝   ╚═╝   ╚═╝  ╚═╝╚═╝╚═╝     ╚══════╝

Stripe Integration Guide

Focus: Cards, wallets, subscriptions, and billing — codeAmani's default payment rail for its US-first customer base.

Overview

Stripe handles card and wallet payments globally: one-time charges, subscription billing, invoicing, and hosted Stripe Checkout. Per CLAUDE.md, Stripe is the default payment rail for every codeAmani project. M-Pesa via the Daraja API is a separate rail used only when a project's primary users are in Kenya — see MPESA_PATTERNS.md. The two are alternatives selected per project, not a primary/secondary pair.

Here is the big picture — the payment lifecycle is a clean, predictable loop:

flowchart LR
  A["Customer pays<br/>card or checkout"] --> B["Your server<br/>creates PaymentIntent"]
  B --> C["Payment Element<br/>confirms payment"]
  C --> D["Stripe sends<br/>signed webhook"]
  D --> E["Your server<br/>verifies signature"]
  E --> F["Update DB<br/>and fulfill"]

Official Documentation

Resource URL
Stripe Docs https://docs.stripe.com
API Reference https://docs.stripe.com/api
Developer Changelog https://docs.stripe.com/changelog
Stripe CLI https://docs.stripe.com/stripe-cli
Webhooks https://docs.stripe.com/webhooks
MCP server https://docs.stripe.com/mcp
Agent skills https://docs.stripe.com/skills
Testing https://docs.stripe.com/testing
Node.js SDK https://github.com/stripe/stripe-node

API versioning — pin it explicitly

Stripe uses a flora-named release model: YYYY-MM-DD.codename. A new codename means breaking changes; monthly releases inside a codename are additive-only and safe to adopt.

Release First version Latest monthly (2026-08-23)
Acacia 2024-09-30.acacia —
Basil 2025-03-31.basil 2025-08-27.basil
Clover 2025-09-30.clover 2026-02-25.clover
Dahlia (current) 2026-03-25.dahlia 2026-07-29.dahlia

stripe-node v22.5.0 ships ApiVersion = '2026-07-29.dahlia'. Always pin the version in code rather than relying on your account default — otherwise a dashboard upgrade silently changes the response shapes your code parses.

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2026-07-29.dahlia",
});

A version string must include the codename suffix. "2025-04-30" on its own is not a valid Stripe API version.

Breaking changes that affect existing code

Change Release What to do
latest_invoice.payment_intent replaced by latest_invoice.confirmation_secret Basil+ Expand latest_invoice.confirmation_secret when creating incomplete subscriptions
Flexible billing mode is the default for new subscriptions Clover 2025-09-30 Set billing_mode: { type: "flexible" } explicitly
Checkout postpones subscription creation until after payment Basil 2025-03-31 Don't assume the subscription exists before checkout.session.completed
Legacy Stripe.js methods removed Dahlia 2026-03-25 Replace handleCardPayment, confirmPaymentIntent, handleFpxPayment, handleCardSetup, confirmSetupIntent, createSource, retrieveSource
initCheckout → initCheckoutElements Dahlia 2026-03-25 Rename the call
Checkout collected_information.tax_ids → tax_id Dahlia 2026-07-29 Rename the field read
total_count expansion removed on list responses Basil 2025-03-31 Count separately

stripe.confirmPayment({ elements, confirmParams }) was not removed — it is still the current method. Only the older intent-specific helpers listed above were.


Environments — Sandbox vs Live

Stripe has two kinds of environment: sandboxes (isolated test environments) and live mode (real money). Payments made in a sandbox are never processed by card networks.

flowchart TB
  subgraph SB["Sandbox · no real money"]
    S1["sk_test_… / pk_test_…"]
    S2["Test cards 4242…"]
    S3["stripe listen → whsec_… (test)"]
  end
  subgraph LV["Live · real money"]
    L1["sk_live_… / pk_live_…"]
    L2["Real cards"]
    L3["Dashboard endpoint → whsec_… (live)"]
  end
  SB -.->|"promote after testing"| LV

Sandboxes

A sandbox is an isolated environment inside your account. Each sandbox has its own data and its own API keys, so teammates can test without colliding.

# Provision a sandbox from the CLI (no account registration required)
stripe sandbox create --help

Access sandboxes from Dashboard → account picker → Sandboxes. You can invite an external collaborator into a single sandbox without granting any live-mode access.

Sandbox limitations that matter to us:

Limitation Impact
Can't test IC+ pricing Cost models must use published rates
Can't connect a platform sandbox to connected-account sandboxes Vertiq's Connect flows can't be fully end-to-end tested across sandboxes — plan a live-mode pilot with a real test seller

Live mode

Live mode requires a fully activated account (business details, bank account, tax info). Before flipping any project to live:

  1. Swap sk_test_ / pk_test_ for sk_live_ / pk_live_.
  2. Register the production webhook endpoint in the Dashboard and use its whsec_... — the one from stripe listen is sandbox-only.
  3. Re-verify apiVersion is pinned in code.
  4. Run one real low-value transaction and refund it.

Keys — test vs live

Stripe keys are environment-scoped and namespaced. A test key can never touch live objects, and vice versa — this is the usual cause of No such payment_intent.

Key Prefix Where it lives Notes
Secret key sk_test_ / sk_live_ Server only Full account access. Never in a client bundle
Publishable key pk_test_ / pk_live_ Browser (safe) Only identifies your account to Stripe.js
Restricted key rk_test_ / rk_live_ Server / agents Scoped permissions — use for MCP + agents
Webhook signing secret whsec_ Server only Per-endpoint; stripe listen prints a sandbox-only one

Rules we follow:


Secrets management with Hazina

codeAmani stores Stripe credentials in Hazina, the local encrypted vault (packages/hazina), not in loose .env files. The agent workflow is zero-exposure: Claude handles references and env-var names only, never values.

Catalog the Stripe secrets once (the user runs these — hidden prompt, values never enter chat):

hazina add stripe/secret_key       --type apikey
hazina add stripe/publishable_key  --type apikey
hazina add stripe/webhook_secret   --type apikey
hazina add stripe/restricted_key   --type apikey   # for MCP / agents

Bind a project's env vars to those references, then inject:

hazina bind <project> --path <projectDir> \
  --add STRIPE_SECRET_KEY=stripe/secret_key \
  --add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=stripe/publishable_key \
  --add STRIPE_WEBHOOK_SECRET=stripe/webhook_secret

hazina inject <project>          # writes gitignored .env.local; reports key NAMES only
hazina push <project> --provider vercel --target production

Rules:


MCP Server Setup

Stripe's official MCP server is now a remote, OAuth-authenticated server at https://mcp.stripe.com. This replaces the older local @stripe/mcp stdio package as the documented default.

claude mcp add --transport http stripe https://mcp.stripe.com/

Then authenticate — this opens a Stripe OAuth consent screen:

claude /mcp

OAuth is preferred over a secret key because it grants granular, user-scoped, revocable access. Review authorized sessions under Dashboard → user settings → OAuth sessions.

For headless/agent contexts that cannot do OAuth, pass a restricted API key (rk_..., never a full sk_...) as a bearer token:

// .mcp.json — prefer OAuth; use this only for non-interactive agents
{
  "mcpServers": {
    "stripe": {
      "url": "https://mcp.stripe.com",
      "headers": { "Authorization": "Bearer ${STRIPE_RESTRICTED_KEY}" }
    }
  }
}

Key MCP Tools

The server exposes generic API tools rather than one tool per endpoint, which keeps the context window small:

Tool Description
stripe_api_search Find Stripe API methods by keyword
stripe_api_details Get parameter detail for a specific method
stripe_api_read Call any Stripe GET method
stripe_api_write Call any POST / PATCH / PUT / DELETE method
search_stripe_documentation Search docs and support articles
stripe_implementation_planner Guided planning for a Stripe integration
get_stripe_account_info Retrieve account details
create_refund Issue a refund
stripe_report Search, retrieve, and create reports

Prompt-injection caution: stripe_api_write can move money. Keep human confirmation enabled for write tools, and be careful combining the Stripe MCP with untrusted content sources.


Stripe CLI Setup

The CLI is distributed on npm as @stripe/cli (v1.50.4). There is no @stripe/stripe-cli package.

npm install -g @stripe/cli

stripe login

# Forward webhooks to your local dev server (prints a test-mode whsec_...)
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger checkout.session.completed

Agent skills

Stripe ships first-party skills that keep an agent's Stripe knowledge current (requires CLI v1.43.3+):

stripe agent setup      # installs stripe-docs, stripe-best-practices, upgrade-stripe
stripe docs /payments   # read any docs page as Markdown in the terminal
stripe docs search "payment intents"
stripe docs api GET /v1/products

Prefer stripe docs over scraping docs.stripe.com — it returns agent-ready Markdown. Note stripe docs requires a valid login; re-run stripe login if you see "The API key provided has expired."


SDK Setup

npm install stripe                                  # server — v22.5.0
npm install @stripe/stripe-js @stripe/react-stripe-js  # client — v9.14.0 / v6.8.2
// lib/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2026-07-29.dahlia",
});

Core Patterns

Payment Intent (one-time charge)

// app/api/payments/create-intent/route.ts
import { NextRequest } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { stripe } from "@/lib/stripe";

export async function POST(req: NextRequest) {
  const { userId } = await auth();
  if (!userId) return new Response("Unauthorized", { status: 401 });

  const { amount, currency = "usd", orderId } = await req.json();

  const paymentIntent = await stripe.paymentIntents.create(
    {
      amount,          // smallest currency unit — 2000 = $20.00
      currency,
      metadata: { userId, orderId },
      automatic_payment_methods: { enabled: true },
    },
    { idempotencyKey: `pi_${orderId}` }, // RequestOptions — last arg
  );

  return Response.json({ clientSecret: paymentIntent.client_secret });
}

Stripe Checkout Session

// app/api/checkout/route.ts
const session = await stripe.checkout.sessions.create({
  mode: "payment",
  line_items: [
    {
      price_data: {
        currency: "usd",
        unit_amount: 2000, // $20.00
        product_data: { name: "codeAmani Pro Plan" },
      },
      quantity: 1,
    },
  ],
  success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${process.env.APP_URL}/pricing`,
  metadata: { userId },
});

return Response.json({ url: session.url });

Subscription

Expand latest_invoice.confirmation_secret — the old latest_invoice.payment_intent expansion no longer exists, and set billing_mode explicitly so a future default change cannot move under you.

const customer = await stripe.customers.create({
  email: userEmail,
  metadata: { userId },
});

const subscription = await stripe.subscriptions.create({
  customer: customer.id,
  items: [{ price: process.env.STRIPE_PRICE_ID! }],
  payment_behavior: "default_incomplete",
  payment_settings: { save_default_payment_method: "on_subscription" },
  billing_mode: { type: "flexible" },
  expand: ["latest_invoice.confirmation_secret"],
});

// Hand this to the browser to confirm with the Payment Element
const clientSecret =
  (subscription.latest_invoice as Stripe.Invoice).confirmation_secret?.client_secret;

Webhook Handler

This is the trustworthy core of fulfillment — verify the signature first, then act:

sequenceDiagram
  participant C as "Customer"
  participant S as "Your server"
  participant ST as "Stripe"
  participant DB as "Database"
  C->>S: Request PaymentIntent
  S->>ST: paymentIntents.create
  ST-->>S: client_secret
  S-->>C: client_secret
  C->>ST: confirmPayment via Payment Element
  ST->>S: Webhook payment_intent.succeeded
  S->>ST: constructEvent verify signature
  S->>DB: Mark payment succeeded
  S-->>ST: Respond 2xx
// app/api/webhooks/stripe/route.ts
import { NextRequest } from "next/server";
import Stripe from "stripe";
import { stripe } from "@/lib/stripe";

export const runtime = "nodejs"; // signature verification needs Node crypto

export async function POST(req: NextRequest) {
  const body = await req.text();                       // RAW body — never req.json()
  const signature = req.headers.get("stripe-signature");
  if (!signature) return new Response("Missing signature", { status: 400 });

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch {
    return new Response("Webhook signature verification failed", { status: 400 });
  }

  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as Stripe.Checkout.Session;
      // Provision access for session.metadata?.userId
      break;
    }
    case "payment_intent.succeeded": {
      const pi = event.data.object as Stripe.PaymentIntent;
      // Mark payment succeeded
      break;
    }
    case "customer.subscription.deleted": {
      const sub = event.data.object as Stripe.Subscription;
      // Downgrade user access
      break;
    }
  }

  return new Response("OK");
}

Idempotency & reconciliation

Network blips, double-clicks, and serverless retries happen — and each one risks charging a customer twice. The fix: pass an idempotency key on every state-changing create call, and dedupe webhook events on their id. Stripe stores the result of the first request under that key, so any retry with the same key returns the original PaymentIntent instead of creating a new charge. Generate one stable key per logical operation (e.g. tied to a cart or order), not per HTTP attempt.

The key goes in the options object — the last argument to any method:

const idempotencyKey = `pi_${orderId}`; // stable across retries

const paymentIntent = await stripe.paymentIntents.create(
  {
    amount,
    currency,
    metadata: { userId, orderId },
    automatic_payment_methods: { enabled: true },
  },
  { idempotencyKey }, // <-- RequestOptions, last arg
);

Fulfillment must be idempotent too. Stripe can deliver the same event more than once, so record each event.id and skip anything already processed before you fulfill:

// inside the webhook handler, after constructEvent succeeds
const alreadyProcessed = await db.webhookEvents.exists(event.id);
if (alreadyProcessed) return new Response("OK"); // dedupe — no-op replay

await db.webhookEvents.insert({ id: event.id, type: event.type });
// ...now safe to fulfill exactly once
flowchart TD
  A["Create PaymentIntent<br/>with idempotencyKey"] --> B{"Key seen<br/>before by Stripe"}
  B -->|"yes"| C["Return original<br/>PaymentIntent · no new charge"]
  B -->|"no"| D["Create new<br/>PaymentIntent"]
  D --> E["Webhook arrives"]
  C --> E
  E --> F{"event.id in<br/>processed log"}
  F -->|"yes"| G["Skip · already fulfilled"]
  F -->|"no"| H["Record id<br/>then fulfill once"]

Source: Idempotent requests. Keys are stored by Stripe and expire after 24 hours — they protect against retries, not against a deliberate re-charge a day later.


Client-Side (Payment Element)

npm install @stripe/stripe-js @stripe/react-stripe-js
// components/CheckoutForm.tsx
"use client";
import { PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js";

export function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!stripe || !elements) return;

    // confirmPayment is current — the legacy handleCardPayment /
    // confirmPaymentIntent helpers were removed in Dahlia (2026-03-25).
    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: { return_url: `${window.location.origin}/payment-success` },
    });

    if (error) {
      // Surface error.message to the user — do not treat this as authoritative
      // failure; the webhook is the source of truth.
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button type="submit" disabled={!stripe}>Pay</button>
    </form>
  );
}
// app/checkout/page.tsx
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

export default function CheckoutPage({ clientSecret }: { clientSecret: string }) {
  return (
    <Elements stripe={stripePromise} options={{ clientSecret }}>
      <CheckoutForm />
    </Elements>
  );
}

Test Cards

Card Number Behavior
4242 4242 4242 4242 Successful payment
4000 0000 0000 9995 Declined (insufficient funds)
4000 0025 0000 3155 3D Secure authentication required
4000 0000 0000 0002 Generic decline

Use any future expiry date and any 3-digit CVC. Full matrix: https://docs.stripe.com/testing


Environment Variables

# Required
STRIPE_SECRET_KEY=sk_live_...          # Never expose — server only
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Optional
STRIPE_PRICE_ID=price_...              # Default subscription price ID
STRIPE_RESTRICTED_KEY=rk_...           # Scoped key for agent/MCP contexts

For local dev use sk_test_... / pk_test_.... The whsec_... printed by stripe listen is test-mode only — production uses the signing secret from the Dashboard endpoint.


Common Use Cases

Use Case Approach
One-time payment PaymentIntent + Payment Element
Hosted checkout checkout.sessions.create → redirect
Subscription billing checkout.sessions.create in subscription mode, or subscriptions.create + billing portal
Card saves setupIntents.create + Payment Methods API
Invoicing invoices.create + invoices.sendInvoice
Refunds refunds.create (also exposed as an MCP tool)

Setting up payments

Single-merchant projects (the default)

Most codeAmani products collect payments for themselves. That is the standard setup covered above:

  1. Activate the Stripe account; capture keys into Hazina.
  2. lib/stripe.ts with a pinned apiVersion.
  3. A server route that creates a Checkout Session or PaymentIntent.
  4. A signature-verifying webhook route that is the only thing that fulfills.
  5. stripe listen locally; a Dashboard endpoint in production.

Marketplaces — Vertiq.market

Vertiq Market is a multi-party marketplace: independent sellers list digital goods, buyers pay, and Vertiq takes a commission. The explicit requirement is that sellers receive the payments and are responsible for them — Vertiq is not the party handling the money.

That maps to exactly one Stripe design: Connect with direct charges onto seller-owned accounts.

flowchart LR
  B["Buyer"] -->|"pays"| SA["Seller's Stripe account<br/>(merchant of record)"]
  SA -->|"application_fee_amount"| V["Vertiq platform<br/>(commission only)"]
  SA -->|"payout"| SB["Seller's bank"]
  SA -.->|"refunds · chargebacks<br/>debit THIS balance"| SA

Why direct charges, not destination charges: with destination charges the money lands in the platform's balance and refunds/chargebacks debit the platform. That is precisely the liability Vertiq is trying not to hold. With direct charges the funds never touch Vertiq's balance — only the commission does.

Property Direct charges (Vertiq) Destination charges (rejected)
Funds land in Seller's balance Platform balance
Merchant of record Seller Platform
Refund debits Seller's balance Platform balance
Chargeback debits Seller's balance Platform balance
Statement descriptor Seller's Platform's
Platform revenue application_fee_amount Amount retained

1. Create seller accounts

New platforms should use the Accounts v2 API. The three settings that assign responsibility away from Vertiq:

Setting Value Effect
defaults.responsibilities.losses_collector stripe Stripe — not Vertiq — is liable for the seller's negative balances
defaults.responsibilities.fees_collector stripe Stripe bills processing fees to the seller directly
dashboard full Seller gets the full Stripe Dashboard and self-serves refunds/disputes
// Accounts v2 — recommended for new platforms
const account = await stripe.v2.core.accounts.create({
  contact_email: sellerEmail,
  dashboard: "full",
  defaults: {
    responsibilities: {
      losses_collector: "stripe",   // Stripe bears seller losses, not Vertiq
      fees_collector: "stripe",     // seller pays Stripe fees directly
    },
  },
  configuration: { merchant: {} },  // enables accepting payments
  include: ["configuration.merchant"],
});

The equivalent on the v1 Accounts API with controller properties:

const account = await stripe.accounts.create({
  email: sellerEmail,
  controller: {
    losses: { payments: "stripe" },      // seller/Stripe bear losses
    fees: { payer: "account" },          // seller pays Stripe fees
    stripe_dashboard: { type: "full" },  // full Dashboard access
    requirement_collection: "stripe",    // Stripe runs KYC
  },
});

defaults.responsibilities cannot be changed after creation, and an account's country is fixed. Get this right on the first seller.

2. Onboard the seller

Stripe runs KYC and identity verification — Vertiq never collects government IDs:

const link = await stripe.accountLinks.create({
  account: account.id,
  type: "account_onboarding",
  refresh_url: `${process.env.APP_URL}/sellers/onboarding/refresh`,
  return_url: `${process.env.APP_URL}/sellers/onboarding/complete`,
});
// redirect the seller to link.url

Before letting a seller list anything, confirm the card_payments capability is active — direct charges require it:

const acct = await stripe.accounts.retrieve(sellerAccountId);
const canSell = acct.capabilities?.card_payments === "active";

3. Take a payment (direct charge + commission)

The seller's account ID goes in the request options, not the params — that is what makes it a direct charge:

// app/api/checkout/route.ts
const COMMISSION_BPS = 1000; // 10.00%

const session = await stripe.checkout.sessions.create(
  {
    mode: "payment",
    line_items: [{ price: listing.stripePriceId, quantity: 1 }],
    payment_intent_data: {
      // Vertiq's cut — transferred to the platform balance
      application_fee_amount: Math.round(listing.amount * COMMISSION_BPS / 10_000),
    },
    success_url: `${process.env.APP_URL}/orders/{CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.APP_URL}/listings/${listing.id}`,
    metadata: { listingId: listing.id, buyerId },
  },
  {
    stripeAccount: seller.stripeAccountId, // ← direct charge on the seller
    idempotencyKey: `order_${orderId}`,
  },
);

4. Receive seller webhooks

Connect events arrive at your platform endpoint with an account field naming the connected account. Register a Connect webhook endpoint and branch on it:

event = stripe.webhooks.constructEvent(body, signature, connectWebhookSecret);

const sellerAccountId = event.account; // present on Connect events
switch (event.type) {
  case "checkout.session.completed":
    // release the digital download for this seller's order
    break;
  case "account.updated":
    // capability changed — re-check card_payments before allowing listings
    break;
}

What Vertiq is still responsible for

Being off the payment liability hook is not the same as having no obligations. State these plainly rather than assuming:

Payment liability and marketplace structure carry legal and tax consequences. Confirm this design with counsel and with Stripe's Connect team before launch — this guide documents the technical mechanism, not legal advice.


Troubleshooting

Issue Fix
No such payment_intent Test vs live key mismatch — the namespaces are separate
Webhook 400 — signature mismatch Use the raw body (req.text()), not parsed JSON
confirmation_secret is undefined You expanded latest_invoice.payment_intent; it was replaced — expand latest_invoice.confirmation_secret
stripe.handleCardPayment is not a function Removed in Dahlia — use confirmCardPayment, or confirmPayment with Elements
Stripe CLI not receiving events Ensure stripe listen is running and the port matches
npm i -g @stripe/stripe-cli 404s Wrong package — it is @stripe/cli
stripe docs says key expired Run stripe login again; stripe docs needs CLI v1.43.3+
publishableKey is not set Verify NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is in .env.local

Stripe developer resources

Resource URL Use it for
Docs home https://docs.stripe.com Everything; .md suffix returns agent-ready Markdown
API reference https://docs.stripe.com/api Exact parameters and response shapes
Developer changelog https://docs.stripe.com/changelog Every API change, filterable by release
Release notes https://docs.stripe.com/changelog/dahlia Current release's breaking changes
API upgrades guide https://docs.stripe.com/upgrades How to move between versions safely
Go-live checklist https://docs.stripe.com/get-started/checklist/go-live Pre-launch review
Testing + test cards https://docs.stripe.com/testing Full card/scenario matrix
Sandboxes https://docs.stripe.com/sandboxes Isolated test environments
Keys & best practices https://docs.stripe.com/keys-best-practices Key hygiene, rotation
Restricted API keys https://docs.stripe.com/keys/restricted-api-keys Scoped keys for agents/CI
Connect https://docs.stripe.com/connect Marketplaces (Vertiq)
Accounts v2 https://docs.stripe.com/connect/accounts-v2 Current Connect account model
Connect pricing https://stripe.com/connect/pricing Platform fee structure
MCP server https://docs.stripe.com/mcp Agent tool access
Agent skills https://docs.stripe.com/skills stripe agent setup
Stripe CLI reference https://docs.stripe.com/cli Every CLI command
Workbench https://dashboard.stripe.com/workbench Live request logs, version upgrades
Status page https://status.stripe.com Incidents
stripe-node https://github.com/stripe/stripe-node Source, changelog, types
Stripe Apps marketplace https://marketplace.stripe.com Prebuilt integrations

Dashboard destinations worth bookmarking: API keys (/apikeys), Webhook endpoints (/webhooks), Workbench overview (/workbench/overview), Connect platform profile (/settings/connect/platform-setup), MCP access (/settings/mcp).


Coding agents — rules & best practices

Stripe ships first-party agent skills; install them so an agent's Stripe knowledge comes from Stripe rather than from training data:

npm i -g @stripe/cli
stripe agent setup     # installs stripe-docs, stripe-best-practices, upgrade-stripe
stripe sandbox create  # working API keys, no account registration needed

Hard rules

  1. Never pass payment_method_types. The one exception is Terminal, which requires payment_method_types: ['card_present']. Omitting it enables dynamic payment methods, so you configure payment methods from the Dashboard and Stripe shows each customer the most relevant eligible options. To restrict, use payment_method_configurations or excluded_payment_method_types — never payment_method_types.
  2. Default to a restricted key (rk_), not a secret key (sk_). Any agent, MCP client, or CI job gets the narrowest scope that works.
  3. Never enable automatic_tax: { enabled: true } without an active tax registration. Without one, Stripe calculates and collects no tax while the integration looks like tax is on — the most common Stripe Tax mistake.
  4. Pin apiVersion in code; don't inherit the account default.
  5. Tag Checkout Sessions on 2026-03-25.dahlia+ with integration_identifier (label plus an 8-random-letter suffix) so flows are comparable in the Dashboard.
  6. Fetch before you write. Use stripe docs / the MCP search_stripe_documentation tool instead of recalling an API shape — Stripe's surface moves every month.
  7. Keep human confirmation on MCP write tools. stripe_api_write can move money; treat any untrusted content in the loop as a prompt-injection risk.

Integration routing

Building… Use
One-time payments Checkout Sessions
Custom embedded payment form Checkout Sessions + Payment Element
Saving a card for later Setup Intents
Marketplace / platform (Vertiq) Accounts v2 (/v2/core/accounts)
Subscriptions / recurring Billing APIs + Checkout Sessions
Usage-based billing (new build) Metronome
Sales tax / VAT / GST Stripe Tax + Registrations API

Version drift warning

The bundled stripe-best-practices skill carries a static version table that can lag the live API. At the 2026-08-23 review the skill (v0.6.3) had caught its API-version line up to 2026-07-29.dahlia, but its Node table still read 22.4.0 while the live stripe-node source was at 22.5.0. (At the prior 2026-08-04 review it lagged on both, reporting 2026-06-24.dahlia / 22.3.0.) When the skill and the live source disagree, the live source wins — verify with:

npm view stripe version
curl -sL https://raw.githubusercontent.com/stripe/stripe-node/master/src/apiVersion.ts

codeAmani notes

Official docs: