Skip to content

feat: add rule-based internal accounts and the sweep failure webhook - #891

Merged
bsiaotickchong merged 16 commits into
mainfrom
08-14-grid-rule-based-accounts-schema
Sep 3, 2026
Merged

feat: add rule-based internal accounts and the sweep failure webhook#891
bsiaotickchong merged 16 commits into
mainfrom
08-14-grid-rule-based-accounts-schema

Conversation

@bsiaotickchong

Copy link
Copy Markdown
Contributor

Summary

Adds the API surface for rule-based internal accounts — an additional account number for an existing customer with a routing rule attached, so incoming payments can be attributed to a specific payer and forwarded automatically.

Every schema change the feature needs is bundled here, in one reviewable PR, rather than arriving in pieces.

What's added

RULE_BASED on InternalAccountType, plus the same value as a type filter on GET /customers/internal-accounts.

POST /internal-accounts — the create verb lives on the bare collection, not under /customers/: the body carries an optional customerId, so a customer-scoped path would describe a request that may name no customer. The listing paths keep their GETs, since a list has to pick a population and a create does not. The body takes type and currency, an optional label, and a sweepRule describing where funds are forwarded: a destination (account id plus an optional payment rail) and optional purposeOfPayment, description, and remittanceInformation. Idempotency-Key is required, matching the other endpoints that mint something irreversible.

Only RULE_BASED is creatable. The other account types are provisioned automatically when a customer is created or approved, so the endpoint rejects them with a specific message rather than a generic error.

SWEEP.FAILED webhook — fired whenever a settled payment does not reach the rule's destination, including when the balance is below the corridor minimum and is returned to the payer instead. The payload carries both transaction ids, a reason, and an outcome, so an integrator can distinguish "this payment failed" from "and therefore this amount went somewhere else."

Delivery is at-least-once and a redelivery carries a new event id, so the payload documents deduplicating on incomingTransactionId.

label and sweepRule on the InternalAccount response. A rule-based account can now show what it is: the label recorded at creation, and the rule itself — destination, the derived minimumAmount / maximumAmount band, purpose, remittance and any fee override. Both are output-only, and the band is derived from the corridor at read time rather than stored, so what a platform reads is what the forward will actually enforce. There is deliberately no sweepRule.id: the rule has no lifecycle apart from its account, and publishing an id would invite a resource that does not exist.

Two decisions worth a second opinion

DESTINATION_UNAVAILABLE is not included. It appeared in the original design, but nothing in the implementation can produce it — rail validation raises a single condition that NO_ELIGIBLE_RAIL already covers. Publishing a value that never arrives costs a permanently un-removable enum member (adding one is non-breaking; removing one is not) and generates a dead case in every SDK. Adding it later, if a rail ever produces it, is free. Happy to reserve it if you'd rather.

ABOVE_MAXIMUM is included and wasn't in the original design. A balance over the corridor ceiling would otherwise be submitted whole, rejected, and stranded; it now takes the same return path as any other non-success outcome, and this is how the platform is told.

What's deliberately not here

GET /customers/internal-accounts/{id} and DELETE were in the original design but are not implemented. Speccing them now would generate SDK methods that 405, so they're left for whenever the endpoints land.

Verification

make lint passes with 0 errors, and zero warnings or informational findings on any schema added here. All 1,971 $refs resolve, none dangling.

Everything is edited under openapi/ and the bundles regenerated with make buildopenapi.yaml and mintlify/openapi.yaml are output, so a source-only change would be reverted by the next build and a bundle-only change would be reverted just as silently. The route move adds a source path file (openapi/paths/internal_accounts.yaml) split out of the customers path, and the response fields add two source schemas (SweepRule.yaml, SweepRuleDestination.yaml).

Original PR: #835

@vercel

vercel Bot commented Sep 2, 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 3, 2026 9:47pm UTC
grid-wallet-demo Ignored Ignored Preview Sep 3, 2026 9:47pm UTC

Request Review

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds the OpenAPI surface for creating rule-based internal accounts, exposes their sweep rules, and introduces SWEEP.FAILED webhook events.

  • Adds POST /internal-accounts, the RULE_BASED account type, and request/response schemas.
  • Adds sweep-failure reason, outcome, payload, and webhook schemas.
  • Regenerates the root and Mintlify OpenAPI bundles.

Confidence Score: 3/5

The PR should not merge until the create-request required fields are aligned with the intended endpoint contract.

The published schema currently accepts rule-based account requests without the mandatory routing rule while requiring a customer identifier that the stated endpoint design permits callers to omit.

Files Needing Attention: openapi/components/schemas/customers/InternalAccountCreateRequest.yaml, openapi.yaml, mintlify/openapi.yaml

Important Files Changed

Filename Overview
openapi/components/schemas/customers/InternalAccountCreateRequest.yaml Adds the creation payload, but its required-field list contradicts both the sweep-rule requirement and the stated optional-customer contract.
openapi/paths/internal_accounts.yaml Adds the authenticated, idempotent create operation and documents its success and error responses.
openapi/components/schemas/customers/InternalAccount.yaml Extends internal-account responses with an optional label and sweep rule.
openapi/components/schemas/webhooks/SweepFailure.yaml Defines sweep-failure transaction, reason, outcome, and destination data without an established blocking defect.
openapi/webhooks/sweep.yaml Registers and documents delivery, authentication, correlation, and deduplication semantics for SWEEP.FAILED.
openapi/openapi.yaml Registers the new collection path and sweep webhook in the modular OpenAPI entry point.
openapi.yaml Regenerated published bundle faithfully propagates the two create-request contract mismatches.
mintlify/openapi.yaml Regenerated documentation bundle faithfully propagates the two create-request contract mismatches.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Grid
  participant Destination
  participant WebhookReceiver
  Client->>Grid: POST /internal-accounts
  Grid-->>Client: Rule-based internal account
  Grid->>Destination: Forward settled incoming payment
  alt Forward succeeds
    Destination-->>Grid: Funds accepted
  else Forward fails
    Grid->>WebhookReceiver: SWEEP.FAILED
    WebhookReceiver-->>Grid: 200 received
  end
Loading
Prompt To Fix All With AI
### Issue 1
openapi/components/schemas/customers/InternalAccountCreateRequest.yaml:7-10
**Required sweep rule omitted**

When a generated client constructs a `RULE_BASED` request without `sweepRule`, the published schema accepts it even though this endpoint rejects the request with HTTP 400.

```suggestion
required:
  - customerId
  - type
  - currency
  - sweepRule
```

### Issue 2
openapi/components/schemas/customers/InternalAccountCreateRequest.yaml:7-10
**Optional customer marked required**

When a caller omits `customerId` as allowed by the endpoint design, generated clients reject the request locally because the schema makes that field mandatory.

```suggestion
required:
  - type
  - currency
```

---

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

Reviews (1): Last reviewed commit: "feat: create internal accounts at POST /..." | Re-trigger Greptile

@ls-bolt
ls-bolt Bot force-pushed the 08-14-grid-rule-based-accounts-schema branch from 0c1fe9f to 577bb9e Compare September 2, 2026 18:26
@github-actions

github-actions Bot commented Sep 2, 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): add RULE_BASED option to type parameter docs in customers

go

feat(api): add RULE_BASED account type, sweep webhook event, label/sweepRule fields

kotlin

feat(api): add RULE_BASED type, label/sweepRule to InternalAccount, sweep webhook

openapi

feat(api): add internal_accounts create method, sweep webhook, label/sweepRule fields

php

feat(api): add RULE_BASED type, label/sweepRule fields to internal accounts, sweep webhook

python

feat(api): add sweep webhook event, RULE_BASED account type, label/sweep_rule fields

ruby

feat(api): add sweep webhook event, RULE_BASED account type, label/sweep_rule fields

typescript

feat(api): add SweepWebhookEvent, RULE_BASED type, label/sweepRule fields to internal accounts
⚠️ grid-openapi studio · code

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

⚠️ grid-ruby 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 ✅

⚠️ 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-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@b5d05cafa3e0eb9d93c6dabfe51be97cba38babe
grid-typescript studio · conflict

Your SDK build had at least one note diagnostic.

⚠️ 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/ff2ab1b48dc25962c1d4b98229670d9f7417dfb3/grid-0.0.1-py3-none-any.whl
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-09-03 21:59:33 UTC

@ls-bolt

ls-bolt Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Revision log — updated in place, latest first.

Revision 7

  • Dropped the failureReason sweep note on both transaction schemas. It said a failed sweep reports one of the SWEEP_* reasons, which stopped being true this round — folding SWEEP_EXECUTION_FAILED into QUOTE_EXECUTION_FAILED and SWEEP_NO_ELIGIBLE_RAIL into ACCOUNT_CANNOT_RECEIVE means a sweep can fail with reasons outside that prefix. The field is back to its original one-line description; the enum entries carry the detail.
Earlier revisions (6)

Revision 6

Four rounds of @shreyav's review, all narrowing the sweep vocabulary. Five reasons down to two.

  • SWEEP_EXECUTION_FAILED removed — the same event as QUOTE_EXECUTION_FAILED. The implementation only distinguished them on whether a send operation existed yet, i.e. whether pricing had succeeded; under the single-operation model the operation exists from deposit time, so that discriminator disappears regardless.
  • SWEEP_BELOW_MINIMUM + SWEEP_ABOVE_MAXIMUMSWEEP_AMOUNT_OUT_OF_RANGE — two members carrying one bit. Which edge was crossed is derivable from the rule's own minimumAmount / maximumAmount.
  • SWEEP_NO_ELIGIBLE_RAIL folded into ACCOUNT_CANNOT_RECEIVE — it fired when the rule named a rail the destination cannot support, which that entry already covers; extended its wording rather than keeping a synonym. Dropped outright from the incoming enum, since rail validation returns early for non-external accounts and an internal destination is the only kind that reads as incoming — the member was unreachable there.
  • RULE_BASED added to the type-filter description on the platform and agent internal-account paths. All three share InternalAccountType.yaml, so it was already a valid filter value; only the customers path said so.
  • receivedAmount uses @shreyav's wording verbatim.

What remains: SWEEP_AMOUNT_OUT_OF_RANGE (outside the corridor's band) and SWEEP_QUOTE_FAILED (Grid could not price it). Every other way a sweep can fail was already named by an existing reason.

Revision 5

A failed sweep now fails the transaction, per @bsiaotickchong.

  • sweepFailureReason is gone. The five reasons join the existing failure enums as SWEEP_BELOW_MINIMUM, SWEEP_ABOVE_MAXIMUM, SWEEP_NO_ELIGIBLE_RAIL, SWEEP_QUOTE_FAILED, SWEEP_EXECUTION_FAILED, on both IncomingTransactionFailureReason and OutgoingTransactionFailureReason. The standalone SweepFailureReason schema is deleted. This also retires the exclusivity rule the previous revision had to document — one field to read, and status: FAILED means what it says.
  • sweepCanonicalAccountId is reverted per @shreyav, pending a generic refund destination. It only ever answered "where did the money go when it couldn't go where you asked", which a returned payout or reversed deposit asks too. The failure enums keep the invariant in prose: a rule-based account never holds a balance, so the funds move to the holder's own account in the same currency.
  • receivedAmount wording now leads with the positive case, per @shreyav — always present except on a sweep that was never priced.

The oasdiff report will still flag receivedAmount becoming optional on IncomingTransaction; that part is unchanged and deliberate. The SWEEP_* additions will appear as enum-value-added warnings on the two failure enums, which is the same class of warning RULE_BASED already produces.

Revision 4

  • Documented how sweepFailureReason and failureReason relate, per @shreyav. They are mutually exclusive by construction and disambiguated by status: failureReason is set only when status is FAILED, while a refused sweep leaves the transaction COMPLETED — the deposit landed, only the onward movement was refused. Each field now states the rule and points at the other, so reading either one is sufficient. Applied to both IncomingTransaction and OutgoingTransaction.

Considered and rejected: folding the sweep reasons into failureReason as a single field. That would put a failureReason on a COMPLETED transaction, which is worse for every integrator not using rule-based accounts. Two fields with an explicit exclusivity rule leaves the common case untouched.

Revision 3

  • sweepRule is out of required per @shreyav. The schema was contradicting itself — the field's description already said "Required when type is RULE_BASED" while required made it unconditional. Same forward-compatibility reasoning as customerId: the coupling belongs in the description, not encoded as a permanent constraint.
  • minimumAmount / maximumAmount now lead with the denomination in bold. It was true of minimumAmount but buried behind "the corridor to the destination can carry", and missing from maximumAmount entirely.
  • Earlier in this round: receivedAmount became optional on IncomingTransaction, and sweepFailureReason / sweepCanonicalAccountId are on both transaction schemas. The two are chosen by destination rather than direction — an internal destination reads as incoming, an external one as outgoing — and a rule's destination can be either, so a sweep must be describable in both.

Note on the oasdiff breaking-change report. The 18 errors are all the same change: receivedAmount became optional on IncomingTransaction, which fans out across every endpoint returning one. That is expected and deliberate. A sweep out of a rule-based account is created when the deposit settles, before its quote locks a rate, so the receiving amount genuinely does not exist yet. OutgoingTransaction already modelled this — receivedAmount was never in its required set, alongside paymentRail and expectedSettlementAt. The alternative was publishing an estimate that silently changes between the created and completed events, which is worse for reconciliation than an absent value. Flagging it as needing API-reviewer approval rather than treating the report as noise.

Revision 2

  • customerId is now optional per @shreyav, so the endpoint's shape stays generic across customer- and platform-owned rule-based accounts. The description carries the current restriction (omitting it is rejected until platform-owned accounts ship) rather than the schema encoding it, so relaxing it later is not a breaking change for generated clients. This reverses an earlier call on this PR where I argued the opposite to Greptile — that reasoning was about a runtime restriction, not the contract's shape.
  • Answered the other three inline, no code change: the allOf wrappers exist because a description sibling to a $ref is dropped (house pattern, 262 uses across the bundle); minimumAmount is denominated in the source account's currency and is unconditionally zero for a same-currency internal destination, with inbound rail fees affecting the balance compared but never the threshold; and SWEEP.FAILED covers the pre-quote failures where no transaction exists to hang a transaction webhook on, plus the reason a failed-transaction event cannot express.

Revision 1

  • Rebased onto main (43 commits behind). Two conflicts, both additive: WebhookType kept WALLET_OPERATION.COMPLETED/FAILED alongside SWEEP.FAILED, and the root spec kept both webhook registrations. Bundles regenerated with make build rather than hand-resolved.
  • sweepRule is now required on InternalAccountCreateRequest — the create handler rejects a RULE_BASED account without one, so the schema was advertising a request the API always refuses.
  • Verified after rebase: /internal-accounts exposes only post, /customers/internal-accounts only get, no dangling $refs, and zero remaining references to POST /customers/internal-accounts. make lint clean.

@ls-bolt

ls-bolt Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

⚡ Review ledger

Round 1

  1. sweepRule omitted from required on InternalAccountCreateRequest → FIXED: 94724fed — added to the required list; the handler rejects a RULE_BASED create without it.
  2. customerId should be optional → NOT APPLICABLE: platform-owned rule-based accounts do not exist; the handler rejects an absent customerId with INVALID_INPUT, so an optional field would advertise a request that always 400s. Corrected the schema description instead.

Round 2

Human review (@shreyav), 4 comments.

  1. Make customerId optional so the endpoint stays generic → FIXED: f412e9dd. Removed from required; description states that omitting it creates a platform-owned account and that this is rejected today. Reverses my earlier position on the same field.
  2. Why all the allOf wrappers? → ANSWERED, no change. A key sibling to $ref is ignored, so a bare $ref + description silently drops the description. It is the established pattern in this spec (AgentAction.yaml, the auth schemas; 262 occurrences bundled). Noted that 3.1 permits $ref siblings, so a spec-wide cleanup is possible but separate.
  3. Is minimumAmount in sending currency, and would a same-currency internal destination ever have one from deposit rail fees? → ANSWERED, no change. Source currency — gen_effective_sweep_bounds reads platform_currency off the source account. Same-currency internal returns minimum=0, maximum=None unconditionally. Inbound rail fees reduce the settled balance being compared but never the threshold, which is priced on the send side.
  4. Why not just send a transaction webhook? → ANSWERED, open to reversal. The below-minimum / above-maximum / no-eligible-rail paths create no transaction at all (pinned by test_divert_to_the_canonical_account_creates_no_grid_operation), so there is nothing for a transaction webhook to describe; reason also distinguishes "payer sent too little" from "your rule's destination is misconfigured", which a failed-transaction event cannot. Flagged as a contract call worth settling now if the preference is to fold it into the transaction stream.

Comment thread openapi/components/schemas/customers/InternalAccountCreateRequest.yaml Outdated
Comment thread openapi/components/schemas/customers/SweepRule.yaml
Comment thread openapi/components/schemas/customers/SweepRule.yaml Outdated
Comment thread openapi/components/schemas/webhooks/SweepFailure.yaml Outdated
@ls-bolt

ls-bolt Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🦣 Congratulations @shreyav - your substantive review earned a Running hyena! (common)

Long limbs made it a pursuit hunter, and one species reached North America.

View your Frost-dex: https://zeus.dev.dev.sparkinfra.net/#/dinodex/shreyav?section=ice-age

bsiaotickchong and others added 9 commits September 3, 2026 19:51
The create verb belongs on the bare collection: the body carries an
optional customerId, so a /customers/ path would describe a request that
may name no customer. The customer and platform paths keep their GETs --
a listing has to pick a population, a create does not.

The response also gains label and sweepRule, with the SweepRule and
SweepRuleDestination schemas they need. Without them a rule-based account
could not express its own rule in any response. Both are output-only, and
the band is derived from the corridor at read time rather than stored, so
what a platform reads is what the forward will enforce. There is
deliberately no sweepRule.id -- the rule has no lifecycle apart from its
account.

Edited under openapi/ and rebuilt: the root openapi.yaml and the mintlify
copy are bundler output, so editing those alone would have been reverted
by the next build.
The handler rejects a RULE_BASED create with no sweepRule, but the schema
marked it optional, so a generated client would construct a request the
API always refuses. A rule-based account with no rule has no meaning --
the type and the rule are created together or not at all.
Two corrections to the rule-based account contract.

The schemas described the movement as a "forward" throughout, which is not the
word the product uses. Everything about a sweep now says sweep. Unrelated uses of
the word elsewhere in the spec (documents, cards, SCA, cancellation, proxying)
are left alone.

`sendTransactionId` also described itself as the outgoing transaction created for
the forward. It is the operation covering both the deposit into the rule-based
account and the sweep out of it, so it says that now.

`HELD_IN_RULE_BASED_ACCOUNT` is gone, and with it the `outcome` field. A sweep
that cannot reach its destination always moves the funds to the customer's
account in the same currency, so there was no second outcome to report and a
single-valued field carried no information. `canonicalAccountId` becomes
non-nullable and required, which is the stronger statement: a rule-based account
never holds a balance. The description no longer promises a retry on the next
deposit — there is no such mechanism.

Co-Authored-By: bsiaotickchong <bsiaotickchong@users.noreply.github.com>
Moving creation to POST /internal-accounts was what made a rule-based account's
owner open: it can be created for a customer or for the platform itself. The
prose still assumed a customer throughout, which contradicts the endpoint it
documents.

The mechanism is now described in terms of the account holder — the routing
rule, the canonical account funds land in when a sweep cannot complete, and the
SWEEP.FAILED prose all read the same way whichever owns the account.

customerId keeps its own note that it is required today, since platform-owned
rule-based accounts are not available yet. That is a statement about what the
endpoint currently accepts rather than about what a rule-based account is.

Co-Authored-By: bsiaotickchong <bsiaotickchong@users.noreply.github.com>
The deposit into a rule-based account and the sweep out of it are legs of one
operation, so incomingTransactionId and sendTransactionId named the same
transaction. They collapse into a single required transactionId.

It is created when the deposit settles — before the corridor bounds are
evaluated — so it is always present, including on the paths that refuse the
sweep before a quote exists. That makes it the dedup key for this at-least-once
event as well as the value that correlates it with OUTGOING_PAYMENT.FAILED for
the same sweep.

Co-Authored-By: bsiaotickchong <bsiaotickchong@users.noreply.github.com>
A rule-based account can be owned by a customer or by the platform, which is
what moving creation to POST /internal-accounts was for. Requiring customerId
in the schema contradicted that and would have been a breaking change to relax
once platform-owned accounts ship.

The field stays documented as rejected-when-omitted today, so the schema
describes the shape the endpoint will keep while the description carries the
current restriction.

Co-Authored-By: shreyav <shreyav@users.noreply.github.com>
SWEEP.FAILED existed because the earlier design made the sweep a separate
transaction, so a sweep refused before it was attempted produced nothing for the
platform to observe. The deposit and the sweep are now one operation, created as
soon as the deposit settles, so the platform already receives a transaction for
every case the webhook covered — a second event carrying the same id is
redundant.

The webhook, its SweepFailure payload, and the SWEEP.FAILED type are removed.
What is not redundant moves onto the transaction itself: sweepFailureReason says
why the destination was not paid, and sweepCanonicalAccountId says where the
funds went instead.

They are separate from failureReason because they are not a failure of the
transaction. The deposit succeeds and the transaction completes; it is the
onward sweep that was refused, and an integrator who sees only a completed
deposit would otherwise have no way to tell BELOW_MINIMUM (the payer sent too
little) from NO_ELIGIBLE_RAIL (the rule's destination is misconfigured).

Co-Authored-By: bsiaotickchong <bsiaotickchong@users.noreply.github.com>
A payment swept out of a rule-based account is created when the deposit settles,
which is before the sweep's quote locks a rate — so its receiving amount does
not exist yet.

OutgoingTransaction already allowed that: receivedAmount is not in its required
set, alongside paymentRail, expectedSettlementAt and settlementTimelineSeconds,
which are all documented as null before they resolve. IncomingTransaction
required it, so the same state was inexpressible there.

The two schemas are chosen by the transaction's destination, not its direction:
an internal destination reads as incoming, an external one as outgoing. A rule's
destination can be either, so both have to describe a sweep that has not been
priced, and both now carry sweepFailureReason and sweepCanonicalAccountId for
the same reason — a sweep can fail with either destination.

Publishing an estimate instead was the alternative. Absence is the truthful
state, and an amount that silently changes between the created and completed
events is worse for reconciliation than one that is plainly not there yet.

Co-Authored-By: bsiaotickchong <bsiaotickchong@users.noreply.github.com>
@ls-bolt
ls-bolt Bot force-pushed the 08-14-grid-rule-based-accounts-schema branch from 0f6e2af to d170b60 Compare September 3, 2026 20:42
@github-actions github-actions Bot added the breaking-change Introduces a breaking change to the OpenAPI spec label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⚠️ Breaking OpenAPI changes detected

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

Errors (18)

  • GET /agents/approvals — the response property data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • GET /agents/me/actions — the response property data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • GET /agents/me/actions/{actionId} — the response property transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /agents/me/quotes/{quoteId}/execute — the response property transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • GET /agents/me/transactions — the response property data/items/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • GET /agents/me/transactions/{transactionId} — the response property oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /agents/{agentId}/actions/{actionId}/approve — the response property transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /agents/{agentId}/actions/{actionId}/reject — the response property transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /sandbox/send — the response property oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /sandbox/uma/receive — the response property allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • GET /transactions — the response property data/items/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • GET /transactions/{transactionId} — the response property oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /transactions/{transactionId}/approve — the response property allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /transactions/{transactionId}/cancel — the response property oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /transactions/{transactionId}/confirm — the response property oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /transactions/{transactionId}/reject — the response property allOf[subschema #2]/receivedAmount became optional for the status 200 [response-property-became-optional].
  • POST /transfer-in — the response property oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 201 [response-property-became-optional].
  • POST /transfer-out — the response property oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/receivedAmount became optional for the status 201 [response-property-became-optional].

Warnings (71)

Show sample
  • GET /agents/approvals — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/approvals — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/approvals — added the new SWEEP_QUOTE_FAILED enum value to the data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/approvals — added the new SWEEP_QUOTE_FAILED enum value to the data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/actions — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/actions — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/actions — added the new SWEEP_QUOTE_FAILED enum value to the data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/actions — added the new SWEEP_QUOTE_FAILED enum value to the data/items/transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/actions/{actionId} — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/actions/{actionId} — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/actions/{actionId} — added the new SWEEP_QUOTE_FAILED enum value to the transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/actions/{actionId} — added the new SWEEP_QUOTE_FAILED enum value to the transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/internal-accounts — added the new RULE_BASED enum value to the data/items/type response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • POST /agents/me/quotes/{quoteId}/execute — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • POST /agents/me/quotes/{quoteId}/execute — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • POST /agents/me/quotes/{quoteId}/execute — added the new SWEEP_QUOTE_FAILED enum value to the transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • POST /agents/me/quotes/{quoteId}/execute — added the new SWEEP_QUOTE_FAILED enum value to the transaction/allOf[#/components/schemas/TransactionOneOf]/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/transactions — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the data/items/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/transactions — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the data/items/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/transactions — added the new SWEEP_QUOTE_FAILED enum value to the data/items/oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/transactions — added the new SWEEP_QUOTE_FAILED enum value to the data/items/oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/transactions/{transactionId} — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/transactions/{transactionId} — added the new SWEEP_AMOUNT_OUT_OF_RANGE enum value to the oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/transactions/{transactionId} — added the new SWEEP_QUOTE_FAILED enum value to the oneOf[subschema #1: Incoming Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • GET /agents/me/transactions/{transactionId} — added the new SWEEP_QUOTE_FAILED enum value to the oneOf[subschema #2: Outgoing Transaction]/allOf[subschema #2]/failureReason response property for the response status 200 [response-property-enum-value-added]. Adding new enum values to response could be unexpected for clients, use x-extensible-enum instead.
  • …and 46 more warnings.

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

sweepRule was in the required set while its own description said "Required when
type is RULE_BASED" — the schema and the prose disagreed. RULE_BASED is the only
type this endpoint creates today, so the two were equivalent in practice, but
encoding it as unconditionally required means relaxing it later is a breaking
change for a generated client. The coupling now lives in the description, which
is where a per-type rule belongs.

minimumAmount and maximumAmount are denominated in the rule-based account's own
currency rather than the destination's. That was true of minimumAmount but buried
mid-paragraph, and absent from maximumAmount entirely. Both now lead with it.

Co-Authored-By: shreyav <shreyav@users.noreply.github.com>
Comment thread openapi/components/schemas/transactions/IncomingTransaction.yaml Outdated
The two were documented independently, leaving an integrator to work out whether
a rule-based transaction could carry both and which to trust.

It cannot. They describe different events and are mutually exclusive by
construction: failureReason is set only when status is FAILED, and a diverted
sweep completes — the deposit landed, only the onward movement was refused. Each
field now says that, and points at the other, so reading either one is enough.

Co-Authored-By: shreyav <shreyav@users.noreply.github.com>
Comment thread openapi/components/schemas/transactions/IncomingTransaction.yaml Outdated
Comment thread openapi/components/schemas/transactions/IncomingTransaction.yaml Outdated
A sweep that cannot reach the rule's destination is a failure of the transaction,
so it belongs in failureReason rather than a field beside it. The five reasons
join both failure enums as SWEEP_*, and the separate sweepFailureReason property
and its SweepFailureReason schema are gone.

This also removes the exclusivity rule the previous revision had to document —
there is one field to read, and status FAILED means what it says.

sweepCanonicalAccountId goes too, at the reviewer's request: a generic refund
destination is coming that will cover where the funds went, for this and for
other cases. The failure enums still say a rule-based account never holds a
balance, so the funds move to the holder's own account in the same currency.

receivedAmount now states the positive case first — always present, except on a
sweep that was never priced.

Co-Authored-By: shreyav <shreyav@users.noreply.github.com>
Co-Authored-By: bsiaotickchong <bsiaotickchong@users.noreply.github.com>
Shorter than the version it replaces and says the same thing.

Co-Authored-By: shreyav <shreyav@users.noreply.github.com>
Comment thread openapi/paths/customers/customers_internal_accounts.yaml
Comment thread openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml Outdated
Comment thread openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml Outdated
Comment thread openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml Outdated
SWEEP_EXECUTION_FAILED described the same event as QUOTE_EXECUTION_FAILED — a
payment that was priced and then failed on the way to settlement. Sweeps report
that one now, like any other payout, and both enums say so.

SWEEP_BELOW_MINIMUM and SWEEP_ABOVE_MAXIMUM collapse into
SWEEP_AMOUNT_OUT_OF_RANGE. Which edge was crossed is derivable from the rule's
own minimumAmount and maximumAmount, so two members were carrying one bit that
the caller can already read.

Three reasons left, and each names something the others do not: the amount was
outside the band, no rail could carry it, or Grid could not price it.

RULE_BASED also joins the type-filter descriptions on the platform and agent
internal-account paths, which shared the enum but not the documentation.

Co-Authored-By: shreyav <shreyav@users.noreply.github.com>
SWEEP_NO_ELIGIBLE_RAIL fired when the rule named a rail the destination account
does not support, which is what ACCOUNT_CANNOT_RECEIVE already describes — "the
account exists but can't accept this payment". That entry now says a rail it
cannot accept counts too, and the sweep-specific member is gone.

It is dropped from the incoming enum outright rather than folded: rail
validation returns early for anything that is not an external account, and an
internal destination is the only kind that reads as an incoming transaction, so
the member was unreachable there.

Two sweep reasons remain, and the split is real: the amount was outside the
corridor's band, or Grid could not price it.

Co-Authored-By: shreyav <shreyav@users.noreply.github.com>
Comment thread openapi/components/schemas/transactions/IncomingTransaction.yaml Outdated
It said a failed sweep reports one of the SWEEP_* reasons, which stopped being
true once SWEEP_EXECUTION_FAILED folded into QUOTE_EXECUTION_FAILED and
SWEEP_NO_ELIGIBLE_RAIL into ACCOUNT_CANNOT_RECEIVE. A sweep can now fail with
either of those, so the field is back to its original one-line description and
the enum entries carry the detail.

Co-Authored-By: shreyav <shreyav@users.noreply.github.com>
@bsiaotickchong
bsiaotickchong merged commit 18fb00f into main Sep 3, 2026
9 of 10 checks passed
@bsiaotickchong
bsiaotickchong deleted the 08-14-grid-rule-based-accounts-schema branch September 3, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bolt breaking-change Introduces a breaking change to the OpenAPI spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants