Skip to content

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

Open
bsiaotickchong wants to merge 3 commits into
mainfrom
08-14-grid-rule-based-accounts-schema
Open

feat: add rule-based internal accounts and the sweep failure webhook#891
bsiaotickchong wants to merge 3 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 2, 2026 6:30pm UTC
grid-wallet-demo Ignored Ignored Preview Sep 2, 2026 6:30pm UTC

Request Review

Copy link
Copy Markdown

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.
@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
⏳ (generating...)```
feat: add rule-based internal accounts and the sweep failure webhook

**go**
⏳ (generating...)```
feat: add rule-based internal accounts and the sweep failure webhook

kotlin
⏳ (generating...)```
feat: add rule-based internal accounts and the sweep failure webhook

**openapi**
⏳ (generating...)```
feat: add rule-based internal accounts and the sweep failure webhook

php
⏳ (generating...)```
feat: add rule-based internal accounts and the sweep failure webhook

**python**
⏳ (generating...)```
feat: add rule-based internal accounts and the sweep failure webhook

ruby
⏳ (generating...)```
feat: add rule-based internal accounts and the sweep failure webhook

**typescript**
⏳ (generating...)```
feat: add rule-based internal accounts and the sweep failure webhook

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

grid-typescript studio
grid-openapi studio
grid-ruby studio
grid-kotlin studio
grid-go studio
grid-python studio
grid-php studio
grid-cli studio

⏳ These are partial results; builds are still running.


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-02 18:41:34 UTC

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.
@ls-bolt

ls-bolt Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Revision log — updated in place, latest first.

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.

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