Skip to content

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

Closed
ls-bolt[bot] wants to merge 2 commits into
mainfrom
08-14-grid-rule-based-accounts-schema
Closed

feat: add rule-based internal accounts and the sweep failure webhook#835
ls-bolt[bot] wants to merge 2 commits into
mainfrom
08-14-grid-rule-based-accounts-schema

Conversation

@ls-bolt

@ls-bolt ls-bolt Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

This PR has been claimed. The active PR is now #891.

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).

@mintlify

mintlify Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

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

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview Aug 14, 2026, 5:33 AM

@vercel

vercel Bot commented Aug 14, 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 2, 2026 12:10am UTC
grid-wallet-demo Ignored Ignored Preview Sep 2, 2026 12:10am UTC

Request Review

@ls-bolt ls-bolt Bot added the bolt label Aug 14, 2026
@ls-bolt
ls-bolt Bot force-pushed the 08-14-grid-rule-based-accounts-schema branch from a0a2b64 to 3ada236 Compare August 14, 2026 05:32
@github-actions github-actions Bot added breaking-change Introduces a breaking change to the OpenAPI spec and removed breaking-change Introduces a breaking change to the OpenAPI spec labels Aug 14, 2026

lightspark-bot commented Aug 14, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

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

cli

feat(api): add RULE_BASED type to customers list_internal_accounts

go

feat(api): add RULE_BASED type to internal accounts, sweep webhook event

kotlin

feat(api): add RULE_BASED type to internal accounts, SWEEP.FAILED webhook event

openapi

feat(api): add customer internal account creation, sweep webhook, RULE_BASED account type

php

feat(api): add RULE_BASED account type and sweep webhook event

python

feat(api): add SweepWebhookEvent, RULE_BASED account type filter

ruby

feat(api): add sweep webhook event, RULE_BASED type to internal accounts

typescript

feat(api): add SweepWebhookEvent, RULE_BASED type to internal accounts

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 new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ✅

npm install https://pkg.stainless.com/s/grid-typescript/6c48bae33a2acd08c1d071a6dca27b8ef7d9033a/dist.tar.gz
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/internal-accounts`
grid-kotlin studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ❗

New diagnostics (2 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/internal-accounts`
💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member.
grid-php studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅lint ✅test ✅

New diagnostics (2 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/internal-accounts`
💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member.
grid-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/internal-accounts`
grid-ruby studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ✅test ✅

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/internal-accounts`
grid-go studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@f331d9bc06ed3e20dcc4f82c00cc4c9ffdd8ac24
New diagnostics (2 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/internal-accounts`
💡 Schema/EnumHasOneMember: Confirm intentional use of `enum` with single member.
grid-python studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/6e24602ede861f9f06a62a438e62b066a1f0d5b2/grid-0.0.1-py3-none-any.whl
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/internal-accounts`
grid-cli studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ❗lint ❗test ❗

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /customers/internal-accounts`

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-08-14 05:37:46 UTC

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.
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.

2 participants