Skip to content

feat(cards): report why a card authorization was declined - #960

Open
whoisglover wants to merge 1 commit into
mainfrom
eng-11677-card-declined-reason
Open

feat(cards): report why a card authorization was declined#960
whoisglover wants to merge 1 commit into
mainfrom
eng-11677-card-declined-reason

Conversation

@whoisglover

@whoisglover whoisglover commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Jira: ENG-11677 under ENG-11645

What this does

When a card authorization is declined, the card transaction reports status: DECLINED. #957 added that status.

A platform can now see that the authorization failed, but not why. A frozen card, a spend cap, and an empty funding source all look the same. Only some of those are something the platform can act on, and it cannot tell them apart.

This PR adds the reason.

How it works

  • New CardDeclinedReason enum: CARD_NOT_ACTIVE, SPEND_LIMIT_EXCEEDED, INSUFFICIENT_FUNDS, NO_ELIGIBLE_FUNDING_SOURCE, BLOCKED, UNSUPPORTED_NETWORK, OTHER.
  • CardTransaction gains cardDeclinedReason. It is present only when status is DECLINED.
  • The CARD_TRANSACTION.DECLINED webhook example carries the reason.
  • Sandbox descriptor suffix 003 documents the reason it produces, which is CARD_NOT_ACTIVE.

It also retires a name that was never real. The docs told platforms a frozen card declines with CARD_PAUSED. No enum has ever had that member. The server's decision vocabulary calls it CARD_NOT_ACTIVE. Every page that said CARD_PAUSED now says cardDeclinedReason: CARD_NOT_ACTIVE.

What has to land with this

The server does not expose the reason yet. It stores it on the decision row, EntCardEventDecision.decline_reason, and the public projection drops it. The webdev change that joins the decline decision to the transaction and emits the field merges in the same window.

One thing to settle before this merges

INSUFFICIENT_FUNDS is in the enum, and nothing produces it.

Four of the seven reasons are real in production today. NO_ELIGIBLE_FUNDING_SOURCE, SPEND_LIMIT_EXCEEDED, BLOCKED, and UNSUPPORTED_NETWORK all come from the shared gates that run after _gen_evaluate in decision/base.py, so they apply on production platforms even though ProductionCardDecisionEngine itself approves everything.

INSUFFICIENT_FUNDS is not one of them. Grid never checks the funding source balance when it decides an authorization. The auth is approved to the card network, and the pull fails afterwards in card_hold_action.py, whose own comment says so: "The auth was already approved to the issuer; if only the pull fails (e.g. the wallet is short) the raw exception propagates as transient so the whole delivery rolls back and Lithic re-delivers." That path resolves as EXCEPTION, and it carries no decline reason.

So the docs in this PR now say that plainly, on the sandbox 002 row and on the pre-funding guide. What is left to decide is the enum itself. Keeping the member means committing to move the balance check to authorization time, which production.py already has a TODO for. Dropping it means adding it back later, which is breaking for strict clients on a response enum.

CARD_NOT_ACTIVE has a smaller version of the same question: only SandboxDecisionEngine checks card status, and no shared gate does, so on production platforms a frozen card is declined by the provider rather than by us.

Tests

make build rebundles cleanly. make lint reports 0 errors. The one new finding is informational and is the known $ref-with-example shape that direction and applicationFee already report. cd cli && npm test passes 80 tests. No CARD_PAUSED remains anywhere under openapi/ or mintlify/snippets/.

History

This is the surviving half of #935. That PR was merged into #934's branch by mistake rather than into main, and #934 has since been rebuilt on main without it.

The other half of #935 was the DECLINED status itself, which #957 shipped independently. ENG-11676 is therefore already delivered and only the reason was left to land.

@vercel

vercel Bot commented Sep 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
grid-flow-builder Ignored Ignored Preview Sep 12, 2026 5:23am UTC
grid-wallet-demo Ignored Ignored Preview Sep 12, 2026 5:23am UTC

Request Review

@mintlify

mintlify Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Grid 🟢 Ready View Preview Sep 12, 2026, 5:24 AM

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

cli

chore(internal): regenerate SDK with no functional changes

go

feat(api): add cardDeclinedReason field to card/transferin/webhook transaction types

kotlin

feat(api): add cardDeclinedReason field to CardTransaction

openapi

feat(api): add cardDeclinedReason field to card transactions

php

feat(api): add cardDeclinedReason field to card transactions

python

feat(api): add card_declined_reason field to card transactions

ruby

feat(api): add card_declined_reason field to CardTransaction

typescript

feat(api): add cardDeclinedReason field to card transactions

Edit this comment to update them. They will appear in their respective SDK's changelogs.

grid-typescript studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗build ⏭️lint ⏭️test ⏭️

grid-openapi studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗

grid-ruby studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗build ⏭️lint ⏭️test ⏭️

grid-kotlin studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗build ⏭️lint ⏭️test ⏭️

grid-go studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗build ⏭️lint ⏭️test ⏭️

go get github.com/stainless-sdks/grid-go@d745084289e11cedef2b5b9a55995b932a438a8e
grid-python studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗build ⏭️lint ⏭️test ⏭️

grid-php studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗lint ⏭️test ⏭️

grid-cli studio · code · diff

Your SDK build had at least one "error" diagnostic, but this did not represent a regression.
generate ❗build ⏭️lint ⏭️test ⏭️


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-09-12 05:26:55 UTC

@whoisglover

Copy link
Copy Markdown
Contributor Author

@greptile review

@greptile-apps

greptile-apps Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The PR should not merge until the insufficient-funds documentation is aligned with the currently supported EXCEPTION behavior or the corresponding server behavior lands.

Findings

  1. P1 Insufficient funds behavior is wrong
Fix with agent prompt
### Issue 1
mintlify/snippets/cards/cardholder-setup.mdx:42-44
This guide says an underfunded source produces a `DECLINED` transaction with `cardDeclinedReason: INSUFFICIENT_FUNDS`. However, the current flow approves the authorization and reports the later failed pull as `EXCEPTION`, which does not carry `cardDeclinedReason`. The sandbox table now documents that behavior for suffix `002`, while its `declinedInsufficientFunds` example still calls the same suffix a decline. Clients following this guidance will wait for a decline reason that the documented implementation cannot emit. This violates the Mintlify directive to publish accurate, tested documentation.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

  • Documents decline reasons across the OpenAPI source, generated bundles, and Mintlify card guides.
  • Clarifies that sandbox suffix 003 produces a CARD_NOT_ACTIVE decline.
  • Corrects suffix 002 to describe the current approved-then-EXCEPTION flow.
  • One remaining documentation inconsistency still promises an insufficient-funds decline that the current flow cannot produce.

Reviews (2) · Last reviewed commit: "feat(cards): report why a card authoriza..."

@whoisglover
whoisglover force-pushed the eng-11677-card-declined-reason branch from 8ba1e7c to d2a624d Compare September 12, 2026 03:55
@whoisglover
whoisglover marked this pull request as ready for review September 12, 2026 03:56
@whoisglover

Copy link
Copy Markdown
Contributor Author

@greptile review

Comment on lines +42 to +44
Cards decline at auth time if the bound funding source can't cover the
transaction. The decline code surfaces as `INSUFFICIENT_FUNDS` and is
visible on the resulting `CardTransaction`. Fund the source the same
transaction. The reason surfaces as `cardDeclinedReason:
INSUFFICIENT_FUNDS` on the resulting `CardTransaction`. Fund the source the same

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Insufficient funds behavior is wrong

This guide says an underfunded source produces a DECLINED transaction with cardDeclinedReason: INSUFFICIENT_FUNDS. However, the current flow approves the authorization and reports the later failed pull as EXCEPTION, which does not carry cardDeclinedReason. The sandbox table now documents that behavior for suffix 002, while its declinedInsufficientFunds example still calls the same suffix a decline. Clients following this guidance will wait for a decline reason that the documented implementation cannot emit. This violates the Mintlify directive to publish accurate, tested documentation.

Context Used: mintlify/CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/snippets/cards/cardholder-setup.mdx
Line: 42-44

Comment:
**Insufficient funds behavior is wrong**

This guide says an underfunded source produces a `DECLINED` transaction with `cardDeclinedReason: INSUFFICIENT_FUNDS`. However, the current flow approves the authorization and reports the later failed pull as `EXCEPTION`, which does not carry `cardDeclinedReason`. The sandbox table now documents that behavior for suffix `002`, while its `declinedInsufficientFunds` example still calls the same suffix a decline. Clients following this guidance will wait for a decline reason that the documented implementation cannot emit. This violates the Mintlify directive to publish accurate, tested documentation.

**Context Used:** mintlify/CLAUDE.md ([source](https://github.com/lightsparkdev/grid-api/blob/main/mintlify/CLAUDE.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@whoisglover
whoisglover force-pushed the eng-11677-card-declined-reason branch from d2a624d to c4870f2 Compare September 12, 2026 05:23
@github-actions github-actions Bot added the breaking-change Introduces a breaking change to the OpenAPI spec label Sep 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Breaking OpenAPI changes detected

oasdiff reports 6 error / 3 warning changes to openapi.yaml.
This PR will need approval from an API reviewer before merge.

Errors (6)

  • GET /cards — removed the required property data/items/fundingSource from the response with the 200 status [response-required-property-removed].
  • POST /cards — added the new required request property fundingSources [new-required-request-property].
  • POST /cards — removed the required property fundingSource from the response with the 201 status [response-required-property-removed].
  • GET /cards/{id} — removed the required property fundingSource from the response with the 200 status [response-required-property-removed].
  • PATCH /cards/{id} — removed the required property fundingSource from the response with the 200 status [response-required-property-removed].
  • POST webhook:card-state-change — added the new required request property allOf[subschema #2]/data/fundingSources [new-required-request-property].

Warnings (3)

Show sample
  • POST /cards — removed the request property fundingSource [request-property-removed].
  • PATCH /cards/{id} — removed the request property fundingSource [request-property-removed].
  • POST webhook:card-state-change — removed the request property allOf[subschema #2]/data/fundingSource [request-property-removed].

Detected by oasdiff. Full report: job summary or the oasdiff-report artifact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Introduces a breaking change to the OpenAPI spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant