Skip to content

feat(quotes): add an ACH statementDescriptor to quotes - #1142

Merged
akanter merged 9 commits into
mainfrom
10-07-ach-statement-descriptor
Oct 9, 2026
Merged

akanter merged 9 commits into
mainfrom
10-07-ach-statement-descriptor

Conversation

@akanter

@akanter akanter commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds an optional top-level statementDescriptor to POST /quotes. It's a label of up to 10 characters, such as PAYROLL, that the counterparty's bank shows on their statement. It is used only on ACH, where it is sent as the NACHA Company Entry Description, and it works for both directions:

  • a payout (ACH credit) to an external US bank account;
  • a pull (ACH debit) from an external US bank account into an internal account.
{
  "source": { "sourceType": "ACCOUNT", "accountId": "InternalAccount:..." },
  "destination": { "destinationType": "ACCOUNT", "accountId": "ExternalAccount:...", "paymentRail": "ACH" },
  "lockedCurrencySide": "SENDING",
  "lockedCurrencyAmount": 12550,
  "statementDescriptor": "PAYROLL",
  "remittanceInformation": "INV-12345"
}

What changed

  • QuoteRequest.statementDescriptor (new StatementDescriptor schema):
    • Printable ASCII, 1–10 characters. Grid uses its default when it's omitted.
    • It sits next to remittanceInformation and purposeOfPayment. remittanceInformation still carries free-text payment details, which ACH puts in the addenda record.
  • Rejected values: the schema rejects blank values, all zeros, MISC, and the descriptions NACHA reserves for system entries (REVERSAL, RETURN FEE, AUTOENROLL, NONSETTLED, RECLAIM), in any letter case and ignoring surrounding spaces. So generated validators reject the same values the API does.
  • Other rails: sending a statementDescriptor on a quote that moves no ACH entry returns 400 INVALID_INPUT.
  • AchRailDetails.statementDescriptor: transactions and webhooks report the Company Entry Description on the entry.
  • remittanceInformation description: now lists what each rail does with it. ACH uses the addenda record, RTP and FedNow the remittance information field, and wire the OBI. On other rails, such as SEPA, PIX, SPEI, and Faster Payments, it is sent as the payment reference on the recipient's bank statement. That reference is often much shorter than 80 characters, so the description tells integrators to put the most important information first. It also points to statementDescriptor for the ACH purpose text.
  • Examples and docs: the same-currency ACH quote example sets it, and there's a new achPullToAccount example. The send-payment guide and snippet and the changelog are updated.

Why a single top-level field

An earlier revision of this PR put the field inside a per-rail railOptions object. It's now a single top-level field instead, because none of the other rails we looked at has a separate short label like this. On those rails, the text the recipient sees is either the sender name or the remittance reference, which remittanceInformation already covers. Several bank-grade ACH APIs use the same split: a 10-character statement descriptor plus a separate addenda field. destination.paymentRail is unchanged and not deprecated.

Test plan

  • make build: bundles regenerated, and openapi.yaml is identical to mintlify/openapi.yaml.
  • make lint (Redocly, Spectral, markdown, and mint openapi-check): 0 errors.
  • oasdiff breaking (1.16.0, the version CI pins) against main: no breaking changes.
  • Validated against the bundled spec with Ajv:
    • StatementDescriptor accepts PAYROLL, ACME PAY, MISC PAY, REVERSALS, 0001, A.
    • It rejects empty, blank, all-zero, MISC/misc, every reserved value in mixed case and with padding, 11 characters, and non-ASCII.
    • A QuoteRequest carrying it validates.
  • Automated pre-PR review: no findings.

@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

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

3 Skipped Deployments
Project Deployment Actions Updated
grid-cards-demo Ignored Ignored Preview Oct 9, 2026 6:59pm UTC
grid-flow-builder Ignored Ignored Preview Oct 9, 2026 6:59pm UTC
grid-wallet-demo Ignored Ignored Preview Oct 9, 2026 6:59pm UTC

Request Review

Copy link
Copy Markdown

This stack of pull requests is managed by Graphite. Learn more about stacking.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Critical impact] The PR appears safe to merge; no actionable issue was found.

Summary

Adds optional statementDescriptor to POST /quotes for ACH payouts and pulls.

  • Limits request values to 1–10 printable ASCII characters and rejects blank, all-zero, and reserved descriptions.
  • Adds the field to ACH transaction details and an outgoing webhook example.
  • Updates quote examples, payment guides, the changelog, and both bundled specs.

No new actionable issues were found. The earlier unnumbered finding concerned a plan and schema design that are no longer present.

Reviews (4) · Last reviewed commit: "feat(quotes): replace railOptions with a..." · Reviewed by Greptile

Comment thread docs/plans/2026-10-07-ach-statement-descriptor.md Outdated
@mintlify

mintlify Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

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

Project Status Preview Updated
Grid 🟢 Ready View Preview Oct 9, 2026, 7:00 PM

@ls-bolt ls-bolt Bot changed the title plan: ACH companyEntryDescription via quote destination railOptions feat(quotes): select the rail with railOptions and set the ACH companyEntryDescription Oct 7, 2026
@ls-bolt

ls-bolt Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

⚡ Review ledger

Round 1

  1. Other rails accept ACH text: BasicRailOptions must reject extra fields → FIXED: additionalProperties: false on AchRailOptions and BasicRailOptions (test: Ajv check rejects WIRE + companyEntryDescription)

Round 2

  1. P2: the companyEntryDescription schema accepts values it documents as invalid → FIXED: 42f1154, not.pattern in CompanyEntryDescription.yaml (test: 48-case Ajv check against the bundled spec, payout and pull)

Round 3

  1. P2 (held, non-blocking): AccountQuoteSource promises a 400 for railOptions on a REALTIME_FUNDING source → FIXED: description wording now scopes the rejection to accountId being any other account. REALTIME_FUNDING is a separate schema with no railOptions field.

Round 4

  1. P2 (held, non-blocking): AccountQuoteSource promises a 400 for railOptions on a REALTIME_FUNDING source → FIXED: 5478ad2. The description now scopes the rejection to accountId being any other account. REALTIME_FUNDING is a separate schema with no railOptions field.

Round 5

  1. Should pull-source options be a oneOf, as on the destination, to leave room for other pull methods (e.g. a Spark embedded-wallet pull)? → FIXED: 8178019, new PullRailOptionsOneOf (discriminated by paymentRail, AchPullRailOptions its only member)

@ls-bolt

ls-bolt Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

⚡ Review rounds — updated in place, latest first.

Round 9 · b7bcb0f

  • feat(quotes): replace railOptions with a top-level ACH statementDescriptor, per @akanter's direction after the review of other payment rails:
    • Removed railOptions (RailOptionsOneOf, AchRailOptions, BasicRailOptions, PullRailOptionsOneOf, AchPullRailOptions) and restored AccountDestination, AccountQuoteSource and PaymentRail to main, so destination.paymentRail is no longer deprecated.
    • Added an optional QuoteRequest.statementDescriptor, documented as ACH-only and as the NACHA Company Entry Description, for both payouts and pulls.
    • Renamed AchRailDetails.companyEntryDescription to statementDescriptor.
  • Rebased onto main.
  • Verified: lint, matching bundles, oasdiff breaking (none), and 20 Ajv cases. The automated pre-PR review found nothing.
Earlier rounds (8)

Round 8 · 8178019

  • feat(quotes): discriminate pull-source railOptions on paymentRail wraps the pull options in a new PullRailOptionsOneOf, discriminated by paymentRail, with AchPullRailOptions as its only member. A future pull method (card, direct debit, embedded wallet) is then a new member rather than a change to the field's type. Per @akanter's question.
  • Verified: lint, matching bundles, oasdiff breaking (none), the structural Ajv checks and the 48 description cases. The automated pre-PR review found nothing.

Round 7 · 5478ad2

  • docs(quotes): scope the pull railOptions rejection to the account it applies to: AccountQuoteSource.railOptions now says the 400 applies when accountId is any other account, such as an internal account, instead of "any other source". A REALTIME_FUNDING source has no railOptions. This addresses the non-blocking finding Faraday held back on its 5/5 round.
  • Verified: lint, matching bundles, oasdiff breaking (none). The change is description text only, so I didn't re-run the adversarial review.

Round 6 · 42f1154

  • docs(quotes): scope the pull railOptions rejection to the account it applies to: AccountQuoteSource.railOptions now says the 400 applies when accountId is any other account, such as an internal account, instead of "any other source". A REALTIME_FUNDING source has no railOptions. This addresses the non-blocking finding Faraday held back on its 5/5 round.
  • Verified: lint, matching bundles, oasdiff breaking (none). The change is description text only, so I didn't re-run the adversarial review.

Round 5 · 42f1154

  • fix(quotes): reject documented-invalid companyEntryDescription values in the schema adds not: {pattern} to CompanyEntryDescription. It covers blank, all zeros, MISC, REVERSAL, RETURN FEE, AUTOENROLL, NONSETTLED and RECLAIM, case-insensitive and ignoring surrounding spaces. It also states the rule precisely in the description. This addresses Faraday's P2.
  • Verified: make lint-openapi, matching bundles, mint openapi-check, oasdiff breaking (none), a 48-case Ajv accept/reject check, and the structural checks. The automated pre-PR review found nothing.

Round 4 · e3b1e63

  • feat(quotes): accept railOptions on an ACH pull source adds AccountQuoteSource.railOptions through a new ACH-only AchPullRailOptions. It moves companyEntryDescription into one shared CompanyEntryDescription schema and adds an achPullToAccount example and a changelog line. Requested on the backend plan review (ACH pull support).
  • Verified: lint, matching bundles, oasdiff breaking (none), and Ajv checks on payout destinations and pull sources (the pull source rejects ACH_SAME_DAY and WIRE). The adversarial review flagged the first version, which reused the payout options type; it's fixed, and round 2 was clean.

Round 3 · e3b1e63

  • feat(quotes): accept railOptions on an ACH pull source adds AccountQuoteSource.railOptions through a new ACH-only AchPullRailOptions. It moves companyEntryDescription into one shared CompanyEntryDescription schema and adds an achPullToAccount example and a changelog line. Requested on the backend plan review (ACH pull support).
  • Verified: lint, matching bundles, oasdiff breaking (none), and Ajv checks on payout destinations and pull sources (the pull source rejects ACH_SAME_DAY and WIRE). The adversarial review flagged the first version, which reused the payout options type; it's fixed, and round 2 was clean.

Round 2 · d1d4d8a

Round 1 · 256559d

Commits

  • 61e22cc plan: ACH companyEntryDescription via quote destination railOptions

  • 03aff7d feat(quotes): select the rail with railOptions and set the ACH companyEntryDescription

  • 256559d feat(quotes): select the rail with railOptions and set the ACH companyEntryDescription

  • Implemented the approved design: destination.railOptions on POST /quotes (AchRailOptions with companyEntryDescription, plus BasicRailOptions); deprecated the flat paymentRail and rejected sending both; added AchRailDetails.companyEntryDescription; updated examples, docs and the changelog.

  • Removed the plan file.

  • Verified: build, lint, mint openapi-check, oasdiff breaking (none), and Ajv payload checks. The automated pre-PR review found nothing.

Comment thread openapi/components/schemas/quotes/StatementDescriptor.yaml
@lightspark-faraday

lightspark-faraday Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Faraday review

fastpass advisory - comment-only, never blocks a merge. To retire a finding, either fix it or reply explaining why it is wrong: a rebuttal gets an agree or disagree answer on the thread, and if you reply again after a disagreement we concede. Acknowledging it ("will fix") or resolving the thread does not retire it on its own. This comment is edited in place every round; inline findings still post as new comments each round.

Score 5/5: converged - nothing blocking open.

fastpass: reviewed b7bcb0f4102f (round 3) - nothing new to post this round.

Round history (4 rounds)
round reviewed (UTC) commit score inline new
1 2026-10-08 17:12 e3b1e6378c04 4/5 1 1
2 2026-10-08 17:28 42f1154c7bc5 5/5 0 0
3 2026-10-09 10:09 b7bcb0f4102f 5/5 (nothing new) (rebased) 0 0
4 2026-10-09 11:52 b7bcb0f4102f 5/5 (nothing new) 0 0

This summary supersedes the per-round summary blocks on the faraday reviews above; those are left in place as history and are not edited.

@ls-bolt

ls-bolt Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📌 Bolt Status

2026-10-08 17:12:20 UTC — ⚡ Agent molten-geyser-yh-3(#3) started


Feedback

@ls-bolt

ls-bolt Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

@greptile review

Comment thread openapi/components/schemas/quotes/AccountQuoteSource.yaml Outdated
akanter and others added 3 commits October 9, 2026 09:00
…yEntryDescription

Co-Authored-By: akanter <akanter@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…yEntryDescription

Co-Authored-By: akanter <akanter@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
akanter and others added 5 commits October 9, 2026 09:00
Co-Authored-By: akanter <akanter@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… in the schema

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…applies to

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: akanter <akanter@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…iptor

Co-Authored-By: akanter <akanter@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ls-bolt
ls-bolt Bot force-pushed the 10-07-ach-statement-descriptor branch from 8178019 to b7bcb0f Compare October 9, 2026 09:00
@ls-bolt ls-bolt Bot changed the title feat(quotes): select the rail with railOptions and set the ACH companyEntryDescription feat(quotes): add an ACH statementDescriptor to quotes Oct 9, 2026
@ls-bolt

ls-bolt Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

@greptile review

…, FedNow, and wire

Co-Authored-By: akanter <akanter@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@akanter
akanter merged commit 405a2d3 into main Oct 9, 2026
9 checks passed
@akanter
akanter deleted the 10-07-ach-statement-descriptor branch October 9, 2026 19:31

This branch was successfully deployed

1 active deployment
staging - mintlify — d646b37c Deployed Oct 9, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants