Diagnose Stripe `card_declined` patterns

Group Stripe payment failures by decline_code. Surface the top causes and the customers affected. Different decline = different fix.

Updated · published

markdown for agents →

difficulty intermediate · time to value 2 minutes · execution on demand

Start from this

Group Stripe payment failures by decline_code from the last 30 days. Surface the top causes and recent affected customers.

Why this matters

Payment failures cluster by reason — and each reason needs a different response:

  • insufficient_funds → customer issue, retry in 3 days
  • do_not_honor → bank issue, retry in 1 day, try different processor
  • card_velocity_exceeded → your retry logic is too aggressive, slow down
  • incorrect_cvc → UX issue, surface a clearer error in your form
  • card_declined (generic) → ambiguous, possibly fraud — different ladder entirely

Most teams treat all payment failures as a single “Stripe error” line in their error tracker. They retry the same way regardless. The result: aggressive retries on real bank declines (annoys customer), gentle retries on velocity-exceeded (just stalls), and no UX fix for CVC errors (which are 100% addressable).

Agentry groups by decline_code in one query and tells you which patterns to fix in code vs which to handle in CS workflow.

What you get

  • Top decline codes from the last 30 days with count and ARR exposure
  • For each: a recommended response strategy (retry timing, UX fix, CS outreach)
  • The specific customers in the top bucket so support can reach out before they churn

Walk through it

You

Group Stripe payment failures by decline_code from the last 30 days. Surface the top causes and recent affected customers.

Agent

Let me query the failures grouped by code.

Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "SELECT properties.decline_code AS code, count() AS cnt, uniq(distinct_id) AS users, sum(toFloat(properties.amount_cents) / 100) AS dollars_failed FROM events WHERE event = 'payment_failed' AND timestamp >= now() - INTERVAL 30 DAY GROUP BY code ORDER BY cnt DESC"
{
  "rows": [
    { "code": "insufficient_funds",       "cnt": 412, "users": 388, "dollars_failed": 28840 },
    { "code": "do_not_honor",             "cnt": 188, "users": 142, "dollars_failed": 14200 },
    { "code": "card_velocity_exceeded",   "cnt":  96, "users":  18, "dollars_failed":  4800 },
    { "code": "incorrect_cvc",            "cnt":  78, "users":  64, "dollars_failed":  4100 },
    { "code": "expired_card",             "cnt":  44, "users":  44, "dollars_failed":  2200 },
    { "code": "card_declined",            "cnt":  31, "users":  28, "dollars_failed":  1860 }
  ]
}
Agent

Three patterns worth your attention:

  1. insufficient_funds (412) — Customer-side, not your bug. Retry strategy should be 1d / 3d / 7d (Stripe’s default Smart Retries handles this). Make sure dunning emails are firing.
  2. card_velocity_exceeded (96, only 18 users) — This is YOUR retry logic. 18 customers got slammed 5x each in 30 days. Your retry intervals are too tight. Stripe’s banks throttle when you retry too fast. Fix: bump min retry interval to 24h.
  3. incorrect_cvc (78) — Pure UX. Your form isn’t surfacing the error clearly enough; customers re-enter the same wrong CVC. Fix: better inline validation.

Let me check who the velocity-exceeded customers are — these are at high churn risk.

Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "SELECT distinct_id, count() AS retry_attempts FROM events WHERE event = 'payment_failed' AND properties.decline_code = 'card_velocity_exceeded' AND timestamp >= now() - INTERVAL 30 DAY GROUP BY distinct_id ORDER BY retry_attempts DESC LIMIT 5"
{
  "rows": [
    { "distinct_id": "[email protected]", "retry_attempts": 12 },
    { "distinct_id": "[email protected]", "retry_attempts": 9 },
    { "distinct_id": "[email protected]", "retry_attempts": 8 }
  ]
}
Agent

Alice got 12 failed attempts. She probably opened 12 support tickets. CS should reach out today and apologize — the failure was on your side, not hers.

The output

Stripe Payment Failures (30d), grouped by decline_code

CODE                         COUNT  USERS  $ EXPOSED   FIX TYPE
insufficient_funds            412    388    $28,840   CS workflow
do_not_honor                  188    142    $14,200   CS workflow
card_velocity_exceeded         96     18     $4,800   ★ YOUR BUG
incorrect_cvc                  78     64     $4,100   ★ UX FIX
expired_card                   44     44     $2,200   email reminder
card_declined (generic)        31     28     $1,860   ambiguous

★ ACTION ITEMS:

  card_velocity_exceeded (your retry logic is too aggressive)
    Affected customers (top 3):
       [email protected]         12 failed retries
       [email protected]             9 failed retries
       [email protected]      8 failed retries
    Fix: bump retry interval to >=24h between attempts.
    Comm: CS apology email to each affected user today.

  incorrect_cvc (UX is hiding the error)
    78 customers re-entered wrong CVC; many gave up.
    Fix: inline real-time validation + clearer error message.

  All others: Stripe Smart Retries + dunning workflow handles these.

Setting it up

You need a webhook handler for Stripe’s payment_intent.payment_failed or charge.failed event that re-emits to Agentry with decline_code:

// /api/webhooks/stripe
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body, req.headers["stripe-signature"]!, STRIPE_WEBHOOK_SECRET
  );

  if (event.type === "payment_intent.payment_failed") {
    const pi = event.data.object;
    const lastError = pi.last_payment_error;

    await fetch(`https://api.agentry.sh/v1/analytics/`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.AGENTRY_SERVER_API_KEY}`,
        "Content-Type": "application/json",
        "User-Agent": "myapp-stripe/1.0",  // REQUIRED — Cloudflare 403s default UAs
      },
      body: JSON.stringify({
        event: "payment_failed",
        distinct_id: pi.receipt_email ?? pi.customer,  // user identifier
        properties: {
          decline_code: lastError?.decline_code ?? "unknown",
          error_code:   lastError?.code,
          amount_cents: pi.amount,
          currency:     pi.currency,
          payment_intent_id: pi.id,
        },
      }),
    });
  }

  res.json({ received: true });
});

For richer grouping, also capture card_country, card_brand, and processor_response_code — useful for diagnosing fraud-vs-real declines down the line.

Variations

  • “Same decline-code breakdown but split by checkout type (one-time vs subscription renewal). Different patterns dominate each.”
  • “Show me the % of insufficient_funds decisions that EVENTUALLY succeed (next retry). My Smart Retries config feels too aggressive.”
  • “For each customer with 3+ card_velocity_exceeded failures, draft a CS apology email offering account credit.”
  • “On demand, compare yesterday’s top decline_code with the prior seven-day average and draft a #payments-oncall summary.”

Adapt this workflow in your own agent.

Ask your agent to map the starter prompt to your saved signal map, live events, and the current HTTP API before answering.

Agentry gives coding agents production context from errors, product analytics, and deploys through one HTTP API. Fetch and read https://agentry.sh/install.md completely, then install Agentry's full first-time telemetry baseline in this repository. I authorize device-code login, saving the owner key only to ~/.agentry/credentials.json, creating one Agentry project for this repository, and read-only inspection of shipped source plus hidden CI/provider configuration. Deeply analyze the product's business logic and complete value flow before choosing telemetry. Save the complete source-backed plan and show me its exact contents and hash. Until I explicitly approve that plan or request a replacement, do not edit app/CI source, place runtime/CI credentials, commit, exercise product traffic, start proof, push, or deploy. After approval, I authorize only the reviewed targets: place the required scoped browser/server/CI credentials through the established environment or secret mechanism, preserve existing telemetry, implement and test the baseline, commit it, push that reviewed commit when the shipped CI/provider path requires it, exercise safe proof paths with test/non-customer data, and perform one deployment through the reviewed shipped CI/provider path. Ask first if proof would charge money, contact a third party, change real customer data, or require new external access. After the plan is saved, immediately before every onboarding state-changing POST, GET current onboarding state, perform only its single returned next_action, then read state again; do not batch or infer later stages. Continue until status is verified, installation_complete is true, and next_action is null. Keep all secrets, source snapshots, proof markers, and scratch files outside the repository.

+ Full access
5.5 Extra High
  1. 1. Open your repo in Codex, Claude Code, Cursor etc.
  2. 2. Paste the install prompt.
  3. 3. Your agent reads the install doc and shows you an implementation plan for approval.