agentleFS
ShanksgShanksg/clawskills/skills/stripe/skill.md

stripe

Skill2 starsChanged 3 months agoMITView on GitHub

Last validated: 2026-10-14 | API version: 2026-09-30.endive (latest stable) REST base URL: https://api.stripe.com/v1/ Assumed product: Stripe Payments + Billing. Stripe Connect (multi-account platform) is covered in the Auth and Recipes sections. ⚠️ Changed 2026-09-30: Stripe opened the Endive release train (2026-09-30.endive), succeeding Dahlia (last Dahlia version: 2026-08-26.dahlia, additive only). Breaking changes in 2026-09-30.endive that affect server-side integrations (changelog): - payment_method_types is no longer writable on PaymentIntent create/update/confirm, SetupIntent create/update, or Checkout Session create — passing it returns 400 payment_method_types_no_longer_supported. Use…

  • Installs packages

20 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use cloudflare.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • ElasticSkill

    cloudflare

    Comprehensive Cloudflare platform skill covering Workers, Pages, storage (KV, D1, R2), AI (Workers AI, Vectorize, Agents SDK), networking (Tunnel, Spectrum), security (WAF, DDoS), and infrastructure-as-code (Terraform, Pulumi). Use for any Cloudflare development task.

    Also in 3 projects that use this

The file

Show its rules and text

We split this skill into the rules it gives an agent

28 rules. 0 are common, 12 are distinctive, and 16 are very specific to this project.

  • 0

    Common

    Most of the 17 edited versions we read keep these.

  • 12

    Distinctive

    This author's own choices. Worth a look.

  • 16

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Create and manage recurring subscriptions with automatic billing, dunning, and trial periods.

    line 29
  2. Distinctive:

    Test the full payment lifecycle with Stripe's sandbox — no real money, no bank involvement.

    line 34
  3. Very specific:

    All monetary amounts are in the smallest currency unit — cents for USD, pence for GBP, yen for JPY (which has no subunit, so 100 = ¥100).

    line 109
  4. Very specific:

    Every Stripe object has a metadata dictionary for storing arbitrary key/value pairs (e.g., your internal IDs).

    line 117
  5. Distinctive:

    Create restricted keys in Dashboard → Developers → API keys → Create restricted key.

    line 133
  6. Distinctive:

    Always pin the API version explicitly to avoid unexpected behaviour when Stripe releases a new default:

    line 151
  7. Very specific:

    Use Idempotency-Key on every POST request to safely retry without double-charging:

    line 175
  8. Very specific:

    Only use on POST — GET and DELETE are inherently idempotent.

    line 188
  9. Distinctive:

    After the client confirms the SetupIntent, the PaymentMethod is attached to the Customer.

    line 254
  10. Distinctive:

    No front-end payment form — Stripe hosts and renders it:

    line 322
  11. Distinctive:

    When the customer updates their payment method:

    line 397
  12. Very specific:

    Always verify the Stripe-Signature header before processing.

    line 450
  13. Very specific:

    Read raw request body — any JSON parsing before construct_event may corrupt the signature.

    line 451
  14. Distinctive:

    All list endpoints return a consistent envelope:

    line 552
  15. Very specific:

    All list endpoints support created[gte] / created[lte] (Unix timestamps):

    line 575
  16. Distinctive:

    Search is rate-limited to 20 read operations per second (stricter than list endpoints).

    line 610
  17. Very specific:

    Use a stable key derived from your internal operation ID, e.g., f"order-{order_id}-payment".

    line 669
  18. Very specific:

    Never log, store, or transmit raw card numbers (number, cvc, full exp_month/exp_year combination).

    line 755
  19. Very specific:

    Never accept card data in your own API endpoint — only accept PaymentMethod IDs (pm_...) or tokens.

    line 756
  20. Distinctive:

    If you find card data in logs, rotate the API keys immediately and notify Stripe.

    line 758
  21. Very specific:

    Use restricted keys for operations that don't need full access (e.g., a reporting service needs only Read on Charges and Invoices).

    line 763
  22. Very specific:

    Use sk_test_ in all non-production environments — a test key can never trigger live charges.

    line 765
  23. Very specific:

    Always verify Stripe-Signature before processing any webhook payload.

    line 769
  24. Very specific:

    Use a separate webhook signing secret (whsec_...) per endpoint — do not reuse across environments.

    line 770
  25. Distinctive:

    Use the minimum Connect account type needed (Express > Standard > Custom — lower customisation = lower compliance burden).

    line 776
  26. Distinctive:

    Never display the connected account's full bank account or SSN to end users.

    line 778
  27. Very specific:

    Every API action is logged in Dashboard → Developers → Logs (7-day retention for all accounts.

    line 782
  28. Very specific:

    Use sandbox keys (sk_test_, pk_test_) and the test card numbers below.

    line 788

“Common” is measured: kept by at least 6 in 10 of the 17 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
skill.md

Stripe Skill

Last validated: 2026-10-14 | API version: 2026-09-30.endive (latest stable) REST base URL: https://api.stripe.com/v1/ Assumed product: Stripe Payments + Billing. Stripe Connect (multi-account platform) is covered in the Auth and Recipes sections.

⚠️ Changed 2026-09-30: Stripe opened the Endive release train (2026-09-30.endive), succeeding Dahlia (last Dahlia version: 2026-08-26.dahlia, additive only). Breaking changes in 2026-09-30.endive that affect server-side integrations (changelog):

  • payment_method_types is no longer writable on PaymentIntent create/update/confirm, SetupIntent create/update, or Checkout Session create — passing it returns 400 payment_method_types_no_longer_supported. Use dynamic payment methods (automatic_payment_methods[enabled]=true, as the recipes below already do), optionally with excluded_payment_method_types, or allowed_payment_method_types for an explicit list. The field remains read-only on the object.
  • billing_cycle_anchor on subscription update/resume and invoices/create_preview now takes an object: billing_cycle_anchor[type]=now instead of billing_cycle_anchor=now.
  • Connect: the generic fraud rejection reason is replaced by specific fraud reasons, platform-initiated rejections are distinguished from Stripe's, and errors are returned for eventually-due requirements — treat these as open enums.
  • The rest (Financial Connections countries → country, PayTo field removals, SEPA billing-address enforcement, BLIK decline codes, Reserve enums, Stripe.js payment request button deprecation) don't touch the REST calls in this doc.

Also new (non-breaking): subscriptions can be paused on demand (POST /v1/subscriptions/{id}/pause, flexible billing mode only) — paused status is no longer only a trial-end outcome; check status_details.

⚠️ Changed 2026-03-25: Stripe opened the Dahlia release train (2026-03-25.dahlia), succeeding Clover. Only the first version in a train carries breaking changes; later Dahlia versions (2026-04-22, 2026-05-27, 2026-06-24, 2026-07-29) are additive. Breaking changes in 2026-03-25.dahlia that affect server-side integrations:

  • Connect / Capabilities API: risk requirements added to capability objects — code that enumerates requirement types must tolerate the new entries.
  • Subscriptions: a new retention_policy cancellation reason can now appear on cancellation_details.reason. Treat the reason field as an open enum.
  • Event destinations: the events_from parameter now takes string values.
  • Issuing: the Visa card reference ID is now optional on token card references.
  • The remaining breaking changes are in Stripe.js / Elements (renamed initCheckout() → initCheckoutElements(), initEmbeddedCheckout() → createEmbeddedCheckoutPage(), elements.update() now returns a Promise) and affect browser code, not the REST calls in this doc.

Older versions (2026-07-29.dahlia, 2026-02-25.clover) still work — Stripe supports old versions indefinitely for existing accounts — but examples below pin 2026-09-30.endive.


What this skill enables

  • Collect one-time payments and save cards for future use without handling raw card data (PCI-compliant via Stripe.js / Payment Element).
  • Create and manage recurring subscriptions with automatic billing, dunning, and trial periods.
  • Issue full or partial refunds and query payment history programmatically.
  • Build hosted checkout flows (Checkout Sessions) that Stripe renders — no front-end payment UI required.
  • Handle post-payment events reliably via webhooks: fulfil orders, provision access, update CRM records.
  • Operate as a platform (Stripe Connect): onboard connected accounts, route payments, split revenue.
  • Test the full payment lifecycle with Stripe's sandbox — no real money, no bank involvement.

Best-fit use cases

Use caseWhy it mattersPrimary objectsTypical triggerSuccess criteria
One-time purchaseCharge a customer once for a product or servicePaymentIntent, PaymentMethod, CustomerUser submits payment formpayment_intent.succeeded webhook received; order fulfilled
Save card for future billingCharge a returning customer without re-entering cardPaymentMethod, CustomerUser checks "save card"PaymentMethod attached to Customer; future charges succeed
Recurring subscriptionMonthly/annual SaaS or membership billingSubscription, Price, Invoice, CustomerUser selects planSubscription active; first invoice paid
Hosted checkout pageDelegate payment UI to StripeCheckout SessionUser clicks "Buy now"checkout.session.completed webhook; payment_status: paid
Issue a refundReverse a charge (full or partial)Refund, ChargeSupport request or automated policyRefund succeeded; charge.refunded webhook received
Failed payment recoveryRetry a failed subscription invoiceInvoice, PaymentIntentinvoice.payment_failed webhookPayment retried; subscription back to active
Multi-vendor marketplaceRoute payments between platform and sellersConnect Account, Transfer, ChargeOrder placed on platformConnected account receives funds; platform fee collected
Payout reportingPull all payouts and match to bank statementPayout, BalanceTransactionFinance reconciliation runEvery payout has matching BalanceTransactions totalling net amount
Metered billingCharge based on usage at end of billing periodSubscriptionItem, UsageRecord, InvoiceUsage event from productInvoice includes correct usage line items
Payment link (no-code)Generate a shareable URL for one-time or recurring paymentPaymentLinkSales or support requestCustomer completes payment via link; webhook confirms

Key concepts & data model

Core objects

ObjectDescriptionID prefix
CustomerA person or business entity; stores payment methods and billing infocus_
PaymentIntentThe authoritative record of a payment attempt; tracks state through to success/failurepi_
PaymentMethodA reusable token representing a card, bank account, or walletpm_
ChargeA legacy object created when a PaymentIntent succeeds; most APIs now return the PaymentIntent directlych_
InvoiceA bill sent to a Customer; created automatically for subscriptionsin_
SubscriptionA recurring billing agreement linking a Customer to one or more Pricessub_
PriceDefines the billing amount, currency, and interval for a Productprice_
ProductA good or service offered; Prices are attached to Productsprod_
RefundA reversal of a Charge, full or partialre_
Checkout SessionA hosted payment page managed by Stripecs_
PaymentLinkA shareable URL backed by a Checkout Session templateplink_
TransferFunds moved from platform balance to a connected accounttr_
PayoutFunds sent from Stripe balance to a bank accountpo_
BalanceTransactionEvery money movement in Stripe (fee, charge, refund, payout)txn_
EventA webhook payload describing a state changeevt_
Connected AccountAn account on your platform (Stripe Connect)acct_

PaymentIntent lifecycle

requires_payment_method → requires_confirmation → requires_action → processing → succeeded
                                                                               ↘ canceled
StatusMeaning
requires_payment_methodNo valid payment method attached yet
requires_confirmationPayment method attached; awaiting explicit confirm call
requires_action3D Secure or redirect authentication needed from the customer
processingBank processing in progress (instant cards transition through this quickly)
succeededPayment captured; funds reserved or settled
canceledCancelled; a new PaymentIntent must be created

Subscription lifecycle

StatusMeaning
trialingIn free trial; no payment yet
activeIn good standing; latest invoice paid
incompleteFirst payment not yet made; customer has 23 hours
incomplete_expiredInitial 23-hour window expired without payment
past_dueLatest renewal invoice failed; automatic retries in progress
unpaidRetries exhausted; subscription remains but no more attempts
pausedTrial ended without a default payment method, or paused on demand via /pause (see status_details)
canceledTerminal; cannot be reactivated

Amounts and currencies

All monetary amounts are in the smallest currency unit — cents for USD, pence for GBP, yen for JPY (which has no subunit, so 100 = ¥100). Always store and pass amounts as integers.

Expandable fields

Any field that returns an ID (e.g., "customer": "cus_xxx") can be expanded to the full object by passing expand[]=customer in the request. Nest with dots: expand[]=latest_invoice.confirmation_secret.

Metadata

Every Stripe object has a metadata dictionary for storing arbitrary key/value pairs (e.g., your internal IDs). Limits: 50 keys per object, key ≤ 40 chars, value ≤ 500 chars.


Authentication & permissions

API key types

Key typePrefixUseNever in
Secret (live)sk_live_Server-side; full API accessClient-side code, logs, version control
Secret (sandbox)sk_test_Server-side; test mode onlyProduction environments
Publishable (live)pk_live_Browser/mobile; only creates tokensServer-side requests
Publishable (sandbox)pk_test_Browser/mobile; test modeProduction environments
Restrictedrk_live_ / rk_test_Server-side; limited to specific resourcesWhen full access is needed

Create restricted keys in Dashboard → Developers → API keys → Create restricted key. Assign None / Read / Write per resource (Customers, PaymentIntents, Subscriptions, etc.).

Authentication header

Stripe uses HTTP Basic auth with the secret key as the username and an empty password. All major HTTP clients also accept Authorization: Bearer sk_...:

# Basic auth (canonical Stripe style)
curl https://api.stripe.com/v1/customers \
  -u "sk_test_51Abc123...:

# Bearer token (equivalent)
curl https://api.stripe.com/v1/customers \
  -H "Authorization: Bearer sk_test_51Abc123..."

API version pinning

Always pin the API version explicitly to avoid unexpected behaviour when Stripe releases a new default:

curl https://api.stripe.com/v1/payment_intents \
  -u "sk_test_...:" \
  -H "Stripe-Version: 2026-09-30.endive" \
  -d amount=2000 \
  -d currency=usd

Set a default version for your account in Dashboard → Developers → API version. The SDK reads your account default unless you override per-request.

Stripe Connect — acting on behalf of a connected account

Pass Stripe-Account: acct_xxx to perform an API call in the context of a connected account using your platform key:

curl https://api.stripe.com/v1/customers \
  -u "sk_live_platform...:" \
  -H "Stripe-Account: acct_1ABCxyzConnected"

Idempotency keys

Use Idempotency-Key on every POST request to safely retry without double-charging:

curl https://api.stripe.com/v1/payment_intents \
  -u "sk_test_...:" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d amount=2000 \
  -d currency=usd
  • Keys are strings up to 255 characters; UUIDs v4 are recommended.
  • Results are cached for 24 hours. Reusing a key after expiry generates a new request.
  • Reusing a key with different parameters returns a 409 Conflict idempotency_error.
  • Only use on POST — GET and DELETE are inherently idempotent.

Common workflows (recipes)

1. Create a one-time payment (PaymentIntent flow)

Server-side — create PaymentIntent:

curl https://api.stripe.com/v1/payment_intents \
  -u "sk_test_...:" \
  -H "Idempotency-Key: order-42-$(date +%s)" \
  -d amount=4999 \
  -d currency=usd \
  -d "automatic_payment_methods[enabled]=true" \
  -d "metadata[order_id]=order_42"

Response (key fields):

{
  "id": "pi_3OxAbc2eZvKYlo2C1hb8mT9k",
  "object": "payment_intent",
  "amount": 4999,
  "currency": "usd",
  "status": "requires_payment_method",
  "client_secret": "pi_3OxAbc...hb8mT9k_secret_Xyz",
  "metadata": { "order_id": "order_42" }
}

Client-side — confirm with Stripe.js:

const { error } = await stripe.confirmPayment({
  elements,                      // Stripe Elements instance
  clientSecret: pi.client_secret,
  confirmParams: {
    return_url: "https://example.com/order/complete"
  }
});

Fulfil on webhook payment_intent.succeeded — see Recipes #7.


2. Save a card for future use

# Step 1 — create or retrieve the Customer
curl https://api.stripe.com/v1/customers \
  -u "sk_test_...:" \
  -d email="alice@example.com" \
  -d "metadata[user_id]=usr_99"

# Step 2 — create a SetupIntent (confirm later via Stripe.js)
curl https://api.stripe.com/v1/setup_intents \
  -u "sk_test_...:" \
  -d customer=cus_PAbcXyz \
  -d "automatic_payment_methods[enabled]=true"

After the client confirms the SetupIntent, the PaymentMethod is attached to the Customer. To charge it later:

curl https://api.stripe.com/v1/payment_intents \
  -u "sk_test_...:" \
  -d amount=2000 \
  -d currency=usd \
  -d customer=cus_PAbcXyz \
  -d payment_method=pm_1ABCdefGhIjKlMnO \
  -d confirm=true \
  -d off_session=true

off_session=true signals that the customer is not present — Stripe will attempt to complete without 3DS if allowed by the card issuer.


3. Create a subscription

# Step 1 — ensure Customer has a default PaymentMethod
curl https://api.stripe.com/v1/customers/cus_PAbcXyz \
  -u "sk_test_...:" \
  -X POST \
  -d invoice_settings[default_payment_method]=pm_1ABCdefGhIjKlMnO

# Step 2 — create the Subscription
curl https://api.stripe.com/v1/subscriptions \
  -u "sk_test_...:" \
  -d customer=cus_PAbcXyz \
  -d "items[0][price]=price_1ABCdefGhIjKlMnO" \
  -d payment_behavior=default_incomplete \
  -d "expand[]=latest_invoice.confirmation_secret"

payment_behavior=default_incomplete starts the subscription in incomplete status — confirm payment client-side using latest_invoice.confirmation_secret.client_secret before it expires (23 hours).

⚠️ Changed 2025-03-31: Since 2025-03-31.basil (and therefore on the 2026-09-30.endive version pinned here), the Invoice object no longer has payment_intent (nor charge / paid) — an invoice can have multiple payments, listed in invoice.payments (expandable; each entry is an InvoicePayment). To collect the first payment, expand latest_invoice.confirmation_secret instead of latest_invoice.payment_intent. current_period_start / current_period_end also moved off the Subscription onto each subscription item: read items.data[].current_period_end. (invoice change, period change)

Response (key fields):

{
  "id": "sub_1ABCdef...",
  "status": "incomplete",
  "items": {
    "data": [
      {
        "id": "si_1ABCdef...",
        "current_period_start": 1737400000,
        "current_period_end": 1740000000
      }
    ]
  },
  "latest_invoice": {
    "id": "in_1ABCdef...",
    "confirmation_secret": {
      "client_secret": "pi_...secret_...",
      "type": "payment_intent"
    }
  }
}

4. Create a hosted Checkout Session

No front-end payment form — Stripe hosts and renders it:

curl https://api.stripe.com/v1/checkout/sessions \
  -u "sk_test_...:" \
  -d mode=payment \
  -d "line_items[0][price]=price_1ABCdefGhIjKlMnO" \
  -d "line_items[0][quantity]=1" \
  -d success_url="https://example.com/success?session_id={CHECKOUT_SESSION_ID}" \
  -d cancel_url="https://example.com/cancel" \
  -d customer_email="alice@example.com"

Redirect the browser to session.url. Stripe handles the payment UI and redirects to success_url on completion. Confirm fulfilment via checkout.session.completed webhook — do not trust the redirect URL alone.

For a subscription Checkout Session, change mode=subscription and pass a recurring Price.


5. Issue a full refund

curl https://api.stripe.com/v1/refunds \
  -u "sk_test_...:" \
  -H "Idempotency-Key: refund-ch_1ABC-$(date +%s)" \
  -d charge=ch_1ABCdefGhIjKlMnO

Partial refund — add amount in cents:

curl https://api.stripe.com/v1/refunds \
  -u "sk_test_...:" \
  -d charge=ch_1ABCdefGhIjKlMnO \
  -d amount=500

Response (key fields):

{
  "id": "re_1ABCdef...",
  "object": "refund",
  "amount": 500,
  "status": "succeeded",
  "charge": "ch_1ABCdef..."
}

Watch for charge.refunded webhook for async confirmation.


6. Handle a failed subscription payment

import stripe

stripe.api_key = "sk_live_..."

# Called from your webhook handler when invoice.payment_failed fires
def handle_payment_failed(invoice_id: str) -> None:
    invoice = stripe.Invoice.retrieve(invoice_id, expand=["subscription"])
    sub = invoice.subscription

    if sub.status == "past_due":
        # Notify the customer to update their payment method
        send_dunning_email(sub.customer, invoice.hosted_invoice_url)

    # Stripe will automatically retry according to your retry schedule
    # (Dashboard → Settings → Billing → Retry schedule)
    # To trigger an immediate retry:
    # stripe.Invoice.pay(invoice_id)

When the customer updates their payment method:

# Attach the new PaymentMethod to the Customer
curl https://api.stripe.com/v1/payment_methods/pm_NEW.../attach \
  -u "sk_live_...:" \
  -d customer=cus_PAbcXyz

# Attempt to pay the open invoice immediately
curl https://api.stripe.com/v1/invoices/in_OPEN.../pay \
  -u "sk_live_...:" \
  -X POST

7. Webhook receiver — verify and dispatch

import stripe
from flask import Flask, request, jsonify

app = Flask(__name__)
stripe.api_key = "sk_live_..."
WEBHOOK_SECRET = "whsec_..."

@app.post("/stripe/webhook")
def webhook():
    sig = request.headers.get("Stripe-Signature")
    try:
        event = stripe.Webhook.construct_event(
            payload=request.data,      # raw bytes — do NOT parse JSON first
            sig_header=sig,
            secret=WEBHOOK_SECRET,
        )
    except stripe.error.SignatureVerificationError:
        return jsonify(error="invalid signature"), 400

    match event["type"]:
        case "payment_intent.succeeded":
            handle_payment_succeeded(event["data"]["object"])
        case "invoice.paid":
            handle_invoice_paid(event["data"]["object"])
        case "customer.subscription.deleted":
            handle_subscription_cancelled(event["data"]["object"])
        case "checkout.session.completed":
            handle_checkout_complete(event["data"]["object"])

    # Must return 2xx within ~30 seconds or Stripe retries
    return jsonify(ok=True), 200

Critical rules:

  • Always verify the Stripe-Signature header before processing.
  • Read raw request body — any JSON parsing before construct_event may corrupt the signature.
  • Return 200 before triggering long-running work (enqueue a job instead).
  • Make handlers idempotent — Stripe may deliver the same event more than once. Use event.id to deduplicate.

8. Stripe Connect — charge on a connected account (destination charge)

# Create a PaymentIntent where funds go to the connected account
# Platform retains application_fee_amount
curl https://api.stripe.com/v1/payment_intents \
  -u "sk_live_platform...:" \
  -d amount=5000 \
  -d currency=usd \
  -d "automatic_payment_methods[enabled]=true" \
  -d transfer_data[destination]=acct_1ConnectedAcct \
  -d application_fee_amount=500

Direct charge (create on the connected account itself):

curl https://api.stripe.com/v1/payment_intents \
  -u "sk_live_platform...:" \
  -H "Stripe-Account: acct_1ConnectedAcct" \
  -d amount=5000 \
  -d currency=usd \
  -d "automatic_payment_methods[enabled]=true"

Create a connected account (Express onboarding):

# Step 1 — create the account
curl https://api.stripe.com/v1/accounts \
  -u "sk_live_platform...:" \
  -d type=express \
  -d country=US \
  -d email="seller@example.com"
# → { "id": "acct_1ABCdef...", ... }

# Step 2 — generate onboarding link
curl https://api.stripe.com/v1/account_links \
  -u "sk_live_platform...:" \
  -d account=acct_1ABCdef \
  -d "refresh_url=https://example.com/reauth" \
  -d "return_url=https://example.com/onboarded" \
  -d type=account_onboarding
# → { "url": "https://connect.stripe.com/setup/e/..." }

9. Retrieve customer payment history (list charges)

import stripe

stripe.api_key = "sk_live_..."

def get_customer_charges(customer_id: str, limit: int = 100) -> list:
    charges = []
    params = {
        "customer": customer_id,
        "limit": 100,
    }
    while True:
        page = stripe.Charge.list(**params)
        charges.extend(page.data)
        if not page.has_more:
            break
        params["starting_after"] = page.data[-1].id
    return charges

10. Cancel a subscription (at period end)

# Cancel at the end of the current billing period (no proration)
curl https://api.stripe.com/v1/subscriptions/sub_1ABCdef... \
  -u "sk_live_...:" \
  -X POST \
  -d cancel_at_period_end=true

# Cancel immediately
curl https://api.stripe.com/v1/subscriptions/sub_1ABCdef... \
  -u "sk_live_...:" \
  -X DELETE

Listen for customer.subscription.deleted webhook to revoke access.


Query patterns & filtering

Cursor-based pagination

All list endpoints return a consistent envelope:

{
  "object": "list",
  "data": [ ... ],
  "has_more": true,
  "url": "/v1/charges"
}

Parameters:

ParameterTypeDefaultMaxDescription
limitinteger10100Items per page
starting_afterstring——ID of last item on previous page (forward pagination)
ending_beforestring——ID of first item on next page (backward pagination)

starting_after and ending_before are mutually exclusive.

Date range filters

All list endpoints support created[gte] / created[lte] (Unix timestamps):

# All charges from the last 7 days
curl "https://api.stripe.com/v1/charges?limit=100&created[gte]=$(date -v-7d +%s)" \
  -u "sk_live_...:"

Other common filter parameters:

EndpointFilterExample
/v1/chargescustomer, payment_intent, created[gte/lte]customer=cus_xxx
/v1/subscriptionscustomer, status, price, current_period_end[gte]status=past_due
/v1/invoicescustomer, subscription, status, due_date[gte]status=open
/v1/payment_intentscustomer, created[gte/lte]customer=cus_xxx
/v1/refundscharge, payment_intent, created[gte/lte]charge=ch_xxx

Auto-pagination (Python SDK)

# Iterates all pages automatically
for charge in stripe.Charge.list(customer="cus_xxx", limit=100).auto_paging_iter():
    process(charge)

Search API

Stripe has a structured Search API for Customers, PaymentIntents, Charges, Subscriptions, and Invoices:

curl "https://api.stripe.com/v1/customers/search?query=email:'alice@example.com'" \
  -u "sk_live_...:"

Search is rate-limited to 20 read operations per second (stricter than list endpoints).


Reliability: rate limits, retries, idempotency

Rate limits

ModeGlobal limitNotes
Live100 req/secPer account; some endpoints are stricter (see below)
Sandbox25 req/secIntentionally lower to surface rate-limit bugs early

Endpoint-specific limits (stricter):

EndpointLimit
Search API20 read ops/sec
Files API20 reads/sec, 20 writes/sec
Meter Events (/v1/billing/meter_events)1,000/sec live
PaymentIntent updates1,000 updates/PaymentIntent/hr
Create Payout15/sec, 30 concurrent
Connect — create account5/sec sandbox, 30/sec live

Rate limit response

HTTP 429 with header:

Stripe-Rate-Limited-Reason: global-rate

Possible values: global-concurrency, global-rate, endpoint-concurrency, endpoint-rate, resource-specific.

Retry with exponential backoff + jitter

import time, random, stripe

def stripe_call_with_retry(fn, *args, max_retries=5, **kwargs):
    for attempt in range(max_retries):
        try:
            return fn(*args, **kwargs)
        except stripe.error.RateLimitError:
            if attempt == max_retries - 1:
                raise
            wait = (2 ** attempt) + random.uniform(0, 1)
            time.sleep(wait)
        except stripe.error.APIConnectionError:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)

Python SDK auto-retry: stripe.max_network_retries = 3 enables built-in retry for network errors and 429s.

Idempotency

  • Include Idempotency-Key on every POST (charge, refund, subscription creation).
  • Use a stable key derived from your internal operation ID, e.g., f"order-{order_id}-payment".
  • Results cached 24 hours; same key = same response (including 5xx errors).
  • Different params + same key = 409 idempotency_error.
stripe.PaymentIntent.create(
    amount=4999,
    currency="usd",
    idempotency_key=f"order-{order_id}-payment-intent",
)

Error handling & troubleshooting

Error object

{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "decline_code": "insufficient_funds",
    "message": "Your card has insufficient funds.",
    "param": null,
    "charge": "ch_1ABCdef...",
    "doc_url": "https://stripe.com/docs/error-codes/card-declined"
  }
}

HTTP status → action

StatusTypeAction
400invalid_request_errorFix the request — missing or malformed parameter. Check error.param.
401api_errorInvalid API key. Verify sk_live_/sk_test_ prefix matches environment.
402card_errorCard declined or transaction failed. Show error.message to the user.
403api_errorInsufficient permissions — use a secret key or check restricted key scopes.
404invalid_request_errorObject not found. Verify the ID prefix matches the endpoint.
409idempotency_errorSame idempotency key reused with different params. Generate a new key.
429—Rate limited. Retry with exponential backoff; see Stripe-Rate-Limited-Reason.
5xxapi_errorStripe infrastructure issue. Retry with backoff; check https://status.stripe.com.

Error type → handler

error.typeMeaningWhat to do
card_errorCard issue (decline, insufficient funds, expired)Show error.message to the end user; do not retry without user action
invalid_request_errorBad API requestFix the code; do not retry unchanged
api_errorStripe-side problemRetry with exponential backoff
idempotency_errorKey reused with different paramsGenerate a new idempotency key

Common decline_code values

decline_codeMeaningUser-facing message
card_declinedGeneric decline from issuer"Your card was declined. Please try a different card."
insufficient_fundsNot enough balance"Your card has insufficient funds."
expired_cardCard expired"Your card is expired. Please update your payment method."
incorrect_cvcWrong CVC"Incorrect security code. Please check and try again."
stolen_card / lost_cardFlagged by issuer"Your card was declined. Please contact your bank."
do_not_honorIssuer catch-all decline"Your card was declined. Please try a different card or contact your bank."
fraudulentStripe Radar blockedLog internally; do not tell user it was flagged as fraud

Troubleshooting checklist

SymptomLikely causeFix
401 UnauthorizedWrong key or test key in prodConfirm key prefix: sk_live_ for production
404 on a valid-looking IDWrong mode (test ID in live mode)pi_test_... IDs only work with sk_test_...
Webhook 400 signature failureRaw body was parsed/modifiedUse request.data (raw bytes) before passing to construct_event
409 idempotency_errorSame key, different paramsGenerate a fresh UUID for the new request
Subscription stuck incompleteCustomer never confirmed paymentSend them the latest_invoice.hosted_invoice_url; confirm within 23 hours
402 on off-session charge3DS requiredHandle requires_action — send payment_intent.client_secret to customer
Webhook events arriving twiceStripe retry after timeoutMake handlers idempotent; deduplicate on event.id

Security & compliance

PCI compliance

Stripe handles PCI scope when you use Stripe.js / Stripe Elements / Payment Element or Checkout Sessions — you never touch raw card numbers. Maintain SAQ A compliance:

  • Never log, store, or transmit raw card numbers (number, cvc, full exp_month/exp_year combination).
  • Never accept card data in your own API endpoint — only accept PaymentMethod IDs (pm_...) or tokens.
  • Serve your payment page over HTTPS.
  • If you find card data in logs, rotate the API keys immediately and notify Stripe.

API key security

  • Store keys in environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault), never in source code.
  • Use restricted keys for operations that don't need full access (e.g., a reporting service needs only Read on Charges and Invoices).
  • Rotate keys immediately on suspected exposure: Dashboard → API keys → Roll key.
  • Use sk_test_ in all non-production environments — a test key can never trigger live charges.

Webhook security

  • Always verify Stripe-Signature before processing any webhook payload.
  • Use a separate webhook signing secret (whsec_...) per endpoint — do not reuse across environments.
  • Store the signing secret in environment variables.
  • Reject webhooks with a timestamp more than 5 minutes old to prevent replay attacks (the SDK does this by default with a 300-second tolerance).

Stripe Connect — platform responsibilities

  • Use the minimum Connect account type needed (Express > Standard > Custom — lower customisation = lower compliance burden).
  • Platforms are responsible for complying with KYC/AML requirements for their connected accounts.
  • Never display the connected account's full bank account or SSN to end users.

Audit trail

Every API action is logged in Dashboard → Developers → Logs (7-day retention for all accounts; extended with Sigma). Use metadata to attach your internal IDs for easier cross-referencing.


Testing checklist

Use sandbox keys (sk_test_, pk_test_) and the test card numbers below. No real money moves.

Test card numbers

ScenarioCard numberExpiryCVC
Payment succeeds4242 4242 4242 4242Any futureAny 3 digits
Payment declined (generic)4000 0000 0000 0002Any futureAny 3 digits
Insufficient funds4000 0000 0000 9995Any futureAny 3 digits
3D Secure required4000 0025 0000 3155Any futureAny 3 digits
3D Secure 2 — frictionless pass4000 0000 0000 3220Any futureAny 3 digits
Card expired4000 0000 0000 0069Any futureAny 3 digits
Incorrect CVC4000 0000 0000 0101Any futureAny 3 digits
Mastercard success5555 5555 5555 4444Any futureAny 3 digits

Trigger test webhook events (Stripe CLI)

# Install: brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to localhost:8000/stripe/webhook

# In a separate terminal:
stripe trigger payment_intent.succeeded
stripe trigger invoice.payment_failed
stripe trigger customer.subscription.deleted
stripe trigger checkout.session.completed

QA checklist

  • PaymentIntent created in sandbox; client_secret returned
  • Test card 4242... → PaymentIntent reaches succeeded
  • Decline card 4000...0002 → 402 with card_error, decline_code: card_declined
  • 3DS card 4000...3155 → PaymentIntent reaches requires_action; completes after challenge
  • Refund issued on succeeded Charge; re_... returned with status: succeeded
  • Subscription created with payment_behavior=default_incomplete; first invoice payment confirmed
  • invoice.payment_failed webhook received and processed without error
  • cancel_at_period_end=true sets flag; customer.subscription.deleted fires at period end
  • Webhook signature verification passes for valid payload; returns 400 for tampered payload
  • Idempotency key: same request sent twice → identical response, no duplicate charge
  • Pagination loop retrieves all pages (has_more=false terminates loop)
  • Rate limit hit in sandbox (25 req/sec); 429 handled with backoff, eventual success

Sources

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.