Stripe Payments skill for Codex

Charge once, fulfil once, from a verified webhook. Codex builds Stripe one-time payments the way production teams do: the server sets the price, fulfilment runs only from a signature-verified webhook, and every write carries an idempotency key. It picks hosted Checkout by default, tests success, decline, 3D Secure and duplicate deliveries with the Stripe CLI, and asks before anything touches live keys or refunds.

  • Integrations
  • $stripe-payments
  • 194 lines
  • 2 files
  • ~2,350 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- stripe-payments

Installs to ~/.agents/skills/stripe-payments/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $stripe-payments, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Integrates one-time Stripe payments with Checkout Sessions or Payment Intents and the Payment Element, verifies webhooks against the raw body, fulfils orders idempotently, and tests end to end with the Stripe CLI and test cards. Use when adding a checkout or pay button, wiring a Stripe webhook endpoint, debugging signature verification failures, handling 3D Secure or async payment methods, or moving a Stripe integration from test mode to live.

Use it when

  • Adding one-time payments to a web app with Stripe
  • A Stripe webhook endpoint returns 400 or fulfils orders twice
  • You need to test card declines, 3DS and async payment methods locally
  • Preparing a Stripe integration for the switch to live mode

Use something else when

  • You are building recurring plans, trials or usage billing: use stripe-billing
  • Money must be split between sellers on a platform: use stripe-connect

How to check the result

Every stripe listen delivery returns 200, a replayed checkout.session.completed event fulfils the order only once, and 3DS and decline test cards end in the right order state.

The mistake it prevents

Parsing the request as JSON before calling constructEvent, so the signature never matches, or fulfilling the order on the success redirect instead of the webhook.

~/.agents/skills/stripe-payments/SKILL.md

--- name: stripe-payments description: Integrates one-time Stripe payments with Checkout Sessions or Payment Intents and the Payment Element, verifies webhooks against the raw body,… license: MIT ---

Done well means: the server decides the amount, the order is fulfilled exactly once from a verified webhook (never from the success redirect), every Stripe write carries an idempotency key, and the whole flow has been exercised in test mode with the Stripe CLI, including a declined card and a 3D Secure card.

Before writing code

  1. Read AGENTS.md, the lockfile and the framework config. Find any existing stripe client, webhook route and order model; extend them instead of adding a second client.
  2. Check the pinned API version: apiVersion in the client constructor, or the account default in the Dashboard. Field shapes change between versions; code against the pinned one.
  3. Confirm keys come from environment variables (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, publishable key for the client). Never hard-code or log them.
  4. Confirm the user is in test mode (sk_test_...). Anything that touches live keys, issues refunds or changes Dashboard settings needs explicit user confirmation first.

Installing stripe (npm or pip) and running the Stripe CLI need network access; in a Codex sandbox with network off, ask for approval or work against what the project already has.

Choose the integration

NeedDefaultWhy
Standard checkout, fastest to shipCheckout Session, hosted page (mode: 'payment')Stripe handles SCA, wallets, tax, receipts
Checkout inside your pageCheckout Session with ui_mode: 'embedded'Same server logic, no redirect
Fully custom UI and flowPaymentIntent + Payment ElementMaximum control, more code to own
Save a card for later off-session chargesSetupIntent, then PaymentIntent with off_session: trueCorrect mandate and SCA handling
SubscriptionsHand off to $stripe-billingDifferent lifecycle and events
Marketplace payouts to sellersHand off to $stripe-connectFunds flow and onboarding

Prefer Checkout unless the user has a concrete reason for a custom form. Use dynamic payment methods (configured in the Dashboard) instead of hard-coding payment_method_types.

Workflow

  • Create Products and Prices (Dashboard or API); store Price IDs in config, not amounts
  • Server endpoint creates the Checkout Session or PaymentIntent from your own cart data
  • Client redirects or mounts the Payment Element with the returned secret
  • Webhook endpoint verifies the signature on the raw body
  • Fulfilment runs from the webhook, keyed on the Stripe object ID, exactly once
  • Test with the CLI: success, decline, 3DS, async method, duplicate delivery
  • Live-mode checklist reviewed with the user before switching keys

Create a Checkout Session (Node)

import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // pin apiVersion if the project does

export async function createCheckout(orderId: string, items: { priceId: string; qty: number }[]) {
  return stripe.checkout.sessions.create(
    {
      mode: "payment",
      line_items: items.map((i) => ({ price: i.priceId, quantity: i.qty })),
      client_reference_id: orderId,          // your order, echoed back in the webhook
      metadata: { orderId },
      success_url: `${process.env.APP_URL}/orders/${orderId}?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.APP_URL}/cart`,
    },
    { idempotencyKey: `checkout-${orderId}` }, // retries return the same session
  );
}

Never accept a price or amount from the browser. Look the cart up server-side and map it to Price IDs, or compute price_data.unit_amount in the smallest currency unit (cents; JPY has no minor unit).

Payment Element (custom flow)

Server: stripe.paymentIntents.create({ amount, currency, automatic_payment_methods: { enabled: true }, metadata: { orderId } }, { idempotencyKey }) and return only client_secret. Client: elements = stripe.elements({ clientSecret }), mount elements.create('payment'), then stripe.confirmPayment({ elements, confirmParams: { return_url } }). On the return page, read payment_intent from the query string and show status, but still fulfil from the webhook.

Webhooks

The signature is computed over the exact bytes Stripe sent. Any body parser that runs first breaks verification.

// Express: raw body for this route only, mounted before express.json()
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(req.body, req.headers["stripe-signature"]!, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch {
    return res.status(400).send("bad signature");
  }
  if (await alreadyProcessed(event.id)) return res.sendStatus(200);

  switch (event.type) {
    case "checkout.session.completed":
    case "checkout.session.async_payment_succeeded": {
      const s = event.data.object as Stripe.Checkout.Session;
      if (s.payment_status === "paid") await fulfil(s.client_reference_id!, s.id);
      break;
    }
    case "checkout.session.async_payment_failed":
      await markFailed((event.data.object as Stripe.Checkout.Session).client_reference_id!);
      break;
    case "payment_intent.succeeded":
      await fulfilFromIntent(event.data.object as Stripe.PaymentIntent);
      break;
  }
  await markProcessed(event.id);
  res.sendStatus(200);
});
  • Next.js App Router: read await req.text() and pass the string to constructEvent.
  • Python: stripe.Webhook.construct_event(request.get_data(), sig_header, endpoint_secret) (Flask) or await request.body() (FastAPI).
  • Return 2xx fast; push slow work (email, PDFs) to a queue. Stripe retries failed deliveries for up to three days in live mode, so handlers must be idempotent.
  • Events arrive out of order and can be duplicated. Make fulfil idempotent on the order (UPDATE orders SET status='paid' WHERE id=$1 AND status<>'paid'), not just on event.id.
  • Subscribe the endpoint only to the event types you handle.

Local testing with the Stripe CLI

stripe login                                            # opens a browser; needs network
stripe listen --forward-to localhost:3000/webhooks/stripe   # prints a whsec_... secret for local use
stripe trigger checkout.session.completed
stripe trigger payment_intent.payment_failed
stripe events resend evt_123                            # replay one event

The whsec_ from stripe listen differs from the Dashboard endpoint secret. Use it locally only.

Test cardBehaviour
4242 4242 4242 4242Succeeds
4000 0025 0000 3155Requires 3D Secure authentication
4000 0000 0000 9995Declined, insufficient funds
4000 0000 0000 0002Generic decline

Any future expiry, any CVC, any postcode. Test-mode only; never type real card numbers anywhere.

Refunds and disputes

  • stripe.refunds.create({ payment_intent, amount? }, { idempotencyKey }). Refunds move money: confirm the order, amount and reason with the user before running one in live mode.
  • Listen to charge.refunded and charge.dispute.created to keep your order state honest.

Gotchas

  • Fulfilling on the success_url page: users close tabs, and the URL can be loaded by anyone.
  • checkout.session.completed with payment_status: 'unpaid' is normal for bank debits and vouchers; wait for async_payment_succeeded.
  • Using JSON.stringify(req.body) for verification: re-serialised JSON never matches.
  • Mixing test and live objects: IDs from one mode do not exist in the other.
  • Rounding: amounts are integers in minor units. 19.99 * 100 is 1998.9999999999998; use Math.round or integer cents throughout.
  • Expanding everything: retrieve with expand: ['line_items'] only where you need it.
  • A webhook endpoint secret per endpoint and per mode; rotating it in the Dashboard breaks verification until the env var is updated.

Going live (confirm each item with the user)

  • Live keys stored in the secret manager, not in .env committed to git
  • Live webhook endpoint created with the same event list; its secret deployed
  • Statement descriptor, business details and payment methods set in the Dashboard
  • Radar rules reviewed; tax collection decided (Stripe Tax or none)
  • One real low-value payment made and refunded by the user, not by the agent

Verification

  • stripe listen output shows each event returning [200].
  • Run stripe trigger checkout.session.completed twice for the same order (or events resend): the order is fulfilled once.
  • A 3DS card completes after the challenge; a decline card leaves the order unpaid with a readable message.
  • Unit-test the handler with a payload signed by stripe.webhooks.generateTestHeaderString.

Report template

**Flow:** Checkout Session (hosted), mode=payment, API version pinned to <version>
**Endpoints:** POST /api/checkout, POST /webhooks/stripe (events: checkout.session.*, charge.refunded)
**Idempotency:** session keyed on orderId; fulfilment guarded by orders.status
**Tested (test mode):** 4242 success, 3155 3DS, 9995 decline, duplicate delivery -> single fulfilment
**Not done / needs user:** live webhook endpoint, live keys, Dashboard branding

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Stripe Payments"
  short_description: "Charge once, fulfil once, from a verified webhook"
  default_prompt: "$stripe-payments Add a Stripe Checkout button to our cart page and mark orders paid from a webhook"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem