feat: add rule-based internal accounts and the sweep failure webhook - #891
feat: add rule-based internal accounts and the sweep failure webhook#891bsiaotickchong wants to merge 3 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
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 SummaryAdds the OpenAPI surface for creating rule-based internal accounts, exposes their sweep rules, and introduces
Confidence Score: 3/5The 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
|
| 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
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
0c1fe9f to
577bb9e
Compare
✱ Stainless preview builds for gridThis PR will update the cli kotlin php ruby 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. |
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.
|
⚡ Revision log — updated in place, latest first. Revision 1
|
|
⚡ Review ledger Round 1
|

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_BASEDonInternalAccountType, plus the same value as atypefilter onGET /customers/internal-accounts.POST /internal-accounts— the create verb lives on the bare collection, not under/customers/: the body carries an optionalcustomerId, so a customer-scoped path would describe a request that may name no customer. The listing paths keep theirGETs, since a list has to pick a population and a create does not. The body takestypeandcurrency, an optionallabel, and asweepRuledescribing where funds are forwarded: adestination(account id plus an optional payment rail) and optionalpurposeOfPayment,description, andremittanceInformation.Idempotency-Keyis required, matching the other endpoints that mint something irreversible.Only
RULE_BASEDis 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.FAILEDwebhook — 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, areason, and anoutcome, 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.labelandsweepRuleon theInternalAccountresponse. A rule-based account can now show what it is: the label recorded at creation, and the rule itself — destination, the derivedminimumAmount/maximumAmountband, 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 nosweepRule.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_UNAVAILABLEis not included. It appeared in the original design, but nothing in the implementation can produce it — rail validation raises a single condition thatNO_ELIGIBLE_RAILalready 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_MAXIMUMis 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}andDELETEwere 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 lintpasses 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 withmake build—openapi.yamlandmintlify/openapi.yamlare 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