Skip to content

feat: document BENEFICIARY_TRUSTED (409) on customer external-account delete - #770

Merged
jklein24 merged 3 commits into
mainfrom
07-30-feat_document_beneficiary_trusted_409_on_customer_external-account_delete
Jul 30, 2026
Merged

feat: document BENEFICIARY_TRUSTED (409) on customer external-account delete#770
jklein24 merged 3 commits into
mainfrom
07-30-feat_document_beneficiary_trusted_409_on_customer_external-account_delete

Conversation

@jklein24

Copy link
Copy Markdown
Contributor

Summary

DELETE /customers/external-accounts/{externalAccountId} can return 409 when the account is currently a trusted beneficiary, but the spec declared only 204/401/404/500. This documents the missing status and its error code.

  • BENEFICIARY_TRUSTED added to the shared Error409 code enum. Because it was absent, a generated client would raise a validation error while deserializing the response instead of surfacing a code the integrator can branch on.
  • The delete operation now declares 409 and its description states the ordering requirement: an account that is currently a trusted beneficiary must be untrusted first, via POST /customers/external-accounts/{externalAccountId}/untrust (and its /confirm), before it can be deleted.

Trust cannot be revoked as a side effect of a delete — untrusting a beneficiary requires the customer to complete a Strong Customer Authentication challenge, which a DELETE has no way to carry. So the delete is refused while trust is in place rather than leaving the account deleted but still trusted.

Both changes are additive and non-breaking, so info.version stays 2025-10-13.

Test plan

  • make lint exits 0 (Redocly + Spectral, --fail-severity=error): 632 findings, 0 errors, all pre-existing warnings/infos.
  • make build rebundled openapi.yaml and mintlify/openapi.yaml; re-running it is a no-op, so the committed bundles byte-match the build output.
  • The 409/BENEFICIARY_TRUSTED pair is covered by tests asserting the status and code at the HTTP boundary, alongside the trust → refused-delete → untrust → successful-delete lifecycle.
  • Diff is 4 files: 2 spec sources + the 2 generated bundles.

Public

Documents a 409 BENEFICIARY_TRUSTED response on DELETE /customers/external-accounts/{externalAccountId}: an external account that is currently a trusted beneficiary must be untrusted before it can be deleted.

Requested by @jklein24

Original PR: #769

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jul 30, 2026 6:52am
grid-wallet-demo Ignored Ignored Preview Jul 30, 2026 6:52am

Request Review

akanter commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

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

cli

docs(api): expand delete usage text for customer external accounts

go

docs(api): clarify delete restrictions for customer external accounts

kotlin

docs(api): clarify deletion constraints for external accounts

openapi

feat(api): add 409 response and BENEFICIARY_TRUSTED enum to external accounts delete

php

docs(api): clarify delete constraint for external accounts

python

docs(api): clarify deletion restrictions in customers external_accounts delete

ruby

docs(api): clarify delete restrictions for external accounts

typescript

docs(api): clarify deletion constraints in external accounts delete method
⚠️ grid-openapi studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️

grid-ruby studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅build ✅lint ✅test ✅

⚠️ grid-go studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@9b036e8276bdf80524f8dd25c15acfa99025692c
⚠️ grid-kotlin studio · code

Your SDK build had a failure in the test CI job, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ❗

⚠️ grid-python studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/63d309cafd9dee3bdd61ad0b94c6c7516632ed0b/grid-0.0.1-py3-none-any.whl
grid-typescript studio · conflict

Your SDK build had at least one note diagnostic.

grid-php studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅lint ✅test ✅

⚠️ grid-cli studio · code

Your SDK build had a failure in the build CI job, which is a regression from the base state.
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-07-30 20:18:59 UTC

Co-Authored-By: jklein24 <jklein24@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ls-bolt

ls-bolt Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Your wording change is better than mine — kept it verbatim. The build check was failing because the bundles are generated from openapi/ and hadn't been re-run since the edit, so openapi.yaml and mintlify/openapi.yaml still carried the old sentence.

Pushed the rebundle (make build, 2-line diff in the two generated files). make lint exits 0.

🤖 linked-graphite(#1) | Feedback

@ls-bolt

ls-bolt Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Build OpenAPI Documentation and Lint Code & Documentation are both green now after the rebundle. The remaining red check, Mintlify Deployment, is not a spec problem — and I verified that rather than assuming it:

Its log shows the deploy itself succeeding and only the last CDN step failing:

Successfully updated deployment
Navigation updated
Starting page revalidation...
Revalidating 172 paths
Failed to revalidate subdomain: ramps-07-30-feat-document-beneficiary-trusted-409-on-custom

And the preview site is in fact live and serving the new response — BENEFICIARY_TRUSTED appears on the rendered delete endpoint page:

So it's a page-revalidation flake on Mintlify's side. It's posted by the Mintlify GitHub app rather than an Actions workflow, so I can't re-run it from here — a push or a nudge from you on their side should clear it. Flagging rather than papering over it in case you'd rather it be green before merging.

🤖 linked-graphite(#1) | Feedback

@jklein24
jklein24 marked this pull request as ready for review July 30, 2026 16:16
@jklein24
jklein24 requested a review from shreyav July 30, 2026 16:16
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

       .       .
    .  |  /|  .
     \ | / | /
  ----\|/--|/----
      /|\  /|\
     /_|_\/_|_\
       /  \
  ☠ RIFFS OF THE TRUSTED BENEFICIARY ☠

UNTRUST, CONFIRM, DELETE — THEN LET THE 409 SILENCE BREAK.

  • Documents the 409 BENEFICIARY_TRUSTED response for customer external-account deletion.
  • Adds BENEFICIARY_TRUSTED to the shared Error409 code enum and description table.
  • Explains the required untrust and confirmation flow before deletion.
  • Regenerates both committed OpenAPI bundles from the modular source.

Confidence Score: 5/5

The PR appears safe to merge with the source specification and both generated bundles consistently documenting the existing conflict response.

The new response references the established shared Error409 schema, the error code is present in both its enum and documentation table, the prescribed untrust/confirm endpoints match the existing API flow, and both generated bundles remain synchronized.

Important Files Changed

Filename Overview
openapi/paths/customers/customers_external_accounts_{externalAccountId}.yaml Adds the documented 409 response and accurate untrust/confirm remediation for deleting a trusted beneficiary.
openapi/components/schemas/errors/Error409.yaml Adds BENEFICIARY_TRUSTED consistently to the shared 409 enum and its documentation table.
openapi.yaml Correctly reflects the source path and Error409 schema changes in the generated root bundle.
mintlify/openapi.yaml Remains synchronized with the generated root OpenAPI bundle for local Mintlify rendering.

Reviews (1): Last reviewed commit: "Rebundle openapi.yaml after the descript..." | Re-trigger Greptile

@ls-bolt

ls-bolt Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📌 Bolt Status

2026-07-30 16:18:30 UTC — ⚡ Agent linked-graphite-2(#2) started


Feedback

@ls-bolt

ls-bolt Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Status after the approval, since the checks still show one red: this is merge-ready.

  • mergeStateStatus is UNSTABLE, not BLOCKED — so the one failing check is not a required one.

  • Build OpenAPI Documentation, Lint Code & Documentation, Detect breaking changes, and preview all pass on 5702a3d.

  • The only red is Mintlify Deployment, and it's a stale status rather than a real failure. Its log ends with Successfully updated deployment / Navigation updated and then fails on the final CDN step (Failed to revalidate subdomain: ramps-07-30-…). I just re-confirmed the published page serves the change correctly:

    https://ramps-07-30-feat-document-beneficiary-trusted-409-on-custom.mintlify.site/api-reference/external-accounts/delete-customer-external-account-by-id → 200, renders BENEFICIARY_TRUSTED.

I tried to re-request that check run directly and got a 403 — it's owned by the Mintlify app, so ls-bolt can't retrigger it. I deliberately did not push an empty commit to shake it loose, since that would invalidate shreyav's approval for a cosmetic status.

One sequencing note: this spec PR should land before webdev #31456 (the implementation), so the documented 409 is published no later than the behavior that returns it.

🤖 linked-graphite(#2) | Feedback

@jklein24
jklein24 merged commit e8cb197 into main Jul 30, 2026
9 of 10 checks passed
@jklein24
jklein24 deleted the 07-30-feat_document_beneficiary_trusted_409_on_customer_external-account_delete branch July 30, 2026 20:12
shreyav added a commit that referenced this pull request Aug 4, 2026
… examples (#793)

## Summary

Two things, both docs-only. Generated bundles untouched.

1. **Documents `GET` and `DELETE` for a single external account** —
`external-accounts.mdx` covered create and list but neither by-id
operation.
2. **Fixes all 85 remaining broken curl auth examples** across 17 files.

**Replaces #359**, which should be closed.

## Part 1 — the new sections

| Section | Covers |
|---|---|
| Retrieving a single external account | `GET
/customers/external-accounts/{externalAccountId}`, full response
example, `404` |
| Deleting an external account | `DELETE
/customers/external-accounts/{externalAccountId}`, `204`, and the `409
BENEFICIARY_TRUSTED` path |

**Why a rewrite rather than a rebase of #359.** That PR has been open
since April and the spec moved under it in four ways:

1. **It documents a `PATCH` endpoint that does not exist.** The path
exposes only `get:` and `delete:` — verified in both the source path
file and the bundled `openapi.yaml`. About a third of that PR describes
an operation the API doesn't have.
2. **`"accountType": "US_ACCOUNT"`** is not a member of
`ExternalAccountType`. The value is `USD_ACCOUNT`.
3. **`"accountCategory": "CHECKING"`** is not a field on
`UsdAccountInfoBase`. The real field is `bankAccountType`.
4. **Delete's contract changed.** #770 added a `409`, which #359
predates.

**The `409` is why the delete section earns its place.**
`BENEFICIARY_TRUSTED` fires when the account is a trusted SCA
beneficiary, and the caller must untrust *and* confirm before delete
succeeds. Nothing at the call site hints at that.

**The GET example includes `paymentRails`** because the response
genuinely carries it — `UsdExternalAccountInfo` composes
`UsdAccountInfo`, which requires it. A short `<Info>` notes it's
returned rather than sent; that distinction has already caused one real
bug.

Every field traces to a schema: `ExternalAccount` for the envelope,
`UsdAccountInfoBase` for account fields, `UsdAccountInfo` for
`paymentRails` (`ACH`/`WIRE`/`RTP`/`FEDNOW`), `UsdBeneficiary` for the
minimum beneficiary shape.

## Part 2 — the curl auth sweep

Greptile flagged the auth header on the new examples. It was right, and
the problem turned out to be repo-wide: **85 curl examples across 17
files** used

```bash
-H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET'
```

which fails **two** independent ways. The single quotes stop the shell
expanding the variables; and Basic auth requires `base64(user:pass)`,
not the raw pair, so the header is rejected even once expanded. Every
one of these failed on copy-paste — which is the entire point of a curl
example.

All 85 now use `-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET"`.

**This converges on the existing majority rather than introducing a
style.** `api-reference/authentication.mdx` documents `curl -u
"{client_id}:{client_secret}"`, and 178 examples across 40 other files
already use `-u`. The `-H` form was the minority pattern.

Biggest concentrations: `snippets/external-accounts.mdx` (26),
`payouts-and-b2b/payment-flow/list-transactions.mdx` (15),
`rewards/developer-guides/listing-transactions.mdx` (8).

## Verification

- **85 replacements, 85 lines added, 85 deleted** — strictly 1:1, no
line lost or merged
- Zero occurrences of the broken form remain
- **Only auth lines changed** — diff contains no other removals, so no
`-H 'Content-Type'` was clobbered
- **The 11 JS/Python `Basic ${credentials}` usages are untouched.**
Those build the header from a pre-encoded value and are correct; the
sweep matched only `-H` lines carrying the raw credential pair
- No curl block ended up with a duplicate `-u`
- MDX component tags and code fences balance across every `.mdx` in
`mintlify/`
- The new JSON example parses
- Generated bundles byte-identical to `main`

**Not verified:** no visual render check, and `make lint` was not run —
it fails on `main` regardless, because `npx spectral lint` resolves to a
stub `spectral@0.0.0` rather than `@stoplight/spectral-cli`. This change
touches no OpenAPI source, so the Mintlify preview is the meaningful
check.

## Still open in this file

`external-accounts.mdx` documents 21 of ~37 account types in
`account-types.ts`. Missing: `XAF`, `BWP`, `AED`, `BDT`, `EGP`, `GHS`,
`GTQ`, `HTG`, `JMD`, `PKR`, `CNY`, plus wallet types `BASE_WALLET`,
`POLYGON_WALLET`, `PLASMA_WALLET`, `SOLANA_WALLET`, `TRON_WALLET`,
`ETHEREUM_WALLET`. Out of scope here, but it's the largest remaining
documentation gap in this area.

---------

Co-authored-by: Claude <noreply@anthropic.com>
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