-
Notifications
You must be signed in to change notification settings - Fork 10
feat: add rule-based internal accounts and the sweep failure webhook #891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
31b39ed
577bb9e
94724fe
f355efd
99e7d44
a55f42b
f412e9d
0f6e2af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| title: Internal Account Create Request | ||
| type: object | ||
| description: >- | ||
| Request body for `POST /internal-accounts`. Only `RULE_BASED` | ||
| accounts can be created directly, and they always carry a `sweepRule`. | ||
| additionalProperties: false | ||
| required: | ||
| - type | ||
| - currency | ||
|
ls-bolt[bot] marked this conversation as resolved.
|
||
| - sweepRule | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we make this optional so the endpoint can stay generic? i know we can't actually make other accounts rn, but helps us in the future
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in Agreed on the reasoning, and it is worth stating why it matters beyond convenience: making it required now and relaxing it later is a breaking change for anyone who generated a client against the strict schema. Optional-but-currently-rejected is the shape the endpoint keeps, so the contract does not move when platform-owned accounts ship. Fair warning that this reverses a call I made earlier on this PR — Greptile flagged the same field and I argued the opposite, on the grounds that the handler refuses an absent |
||
| properties: | ||
| customerId: | ||
| type: string | ||
| description: >- | ||
| The customer the account is created for. Omit it to create a | ||
| platform-owned account. The account holder must already have a verified | ||
| account in the same currency, which is where funds land when a sweep | ||
| cannot be completed. Platform-owned rule-based accounts are not available | ||
| yet, so omitting this is currently rejected. | ||
| example: Customer:019542f5-b3e7-1d02-0000-000000000001 | ||
| type: | ||
| allOf: | ||
| - $ref: ./InternalAccountType.yaml | ||
| description: >- | ||
| Must be `RULE_BASED`. `INTERNAL_FIAT`, `INTERNAL_CRYPTO`, and | ||
| `EMBEDDED_WALLET` accounts are provisioned automatically when a customer | ||
| is created or approved, so they cannot be created through this endpoint. | ||
| example: RULE_BASED | ||
| currency: | ||
| type: string | ||
| description: >- | ||
| Currency code the account is denominated in (ISO 4217). Rule-based | ||
| accounts are currently available in `USD` only. | ||
| example: USD | ||
| label: | ||
| type: string | ||
| maxLength: 255 | ||
| description: >- | ||
| Your own name for the account, echoed back on reads. Useful for | ||
| identifying which payer the account was issued for. | ||
| example: invoice-4417 | ||
| sweepRule: | ||
| allOf: | ||
| - $ref: ./SweepRuleRequest.yaml | ||
| description: >- | ||
| The routing rule for the account. Required when `type` is `RULE_BASED`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| title: Sweep Rule | ||
| type: object | ||
| description: >- | ||
| The routing rule attached to a rule-based account. Returned on the account | ||
| rather than as a resource of its own, because the rule has no lifecycle apart | ||
| from the account. | ||
| required: | ||
| - destination | ||
| properties: | ||
| destination: | ||
| allOf: | ||
| - $ref: SweepRuleDestination.yaml | ||
| description: Where funds that settle into this account are swept. | ||
| minimumAmount: | ||
| allOf: | ||
| - $ref: ../common/CurrencyAmount.yaml | ||
| description: >- | ||
| The smallest balance the corridor to the destination can carry, | ||
| denominated in this account's own currency. A settled balance below it is | ||
| not swept. Zero means no floor applies, which is the case for a | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this denoted in sending currency? would we ever have a minimum for same-currency internal dest because of deposit rail fees?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes — the account's own currency, which is the sending side. On the same-currency internal case: no, and it is unconditional rather than incidental. That branch returns Deposit rail fees do not reach this either, and the reason is a bit subtle: the sweep reads the account's settled balance, which is already net of whatever the inbound rail cost. The floor exists to stop a balance the outbound corridor would reject, so it is priced on the send side only. An inbound fee shrinks the balance being compared, but never the threshold. That is what makes the guarantee hold — a same-currency internal destination always has an exit, so a rule-based account pointed at one can never strand funds regardless of amount. |
||
| same-currency internal destination: that movement is a book transfer with | ||
| no rail, fee, or conversion to justify one. This is current configuration | ||
| rather than a moving estimate — it changes only when the platform's or | ||
| Grid's configuration changes, so there is nothing to re-poll. | ||
| maximumAmount: | ||
| allOf: | ||
| - $ref: ../common/CurrencyAmount.yaml | ||
| description: >- | ||
| The largest balance the corridor to the destination can carry. Null means | ||
| no ceiling applies. A settled balance above it is not swept. | ||
| purposeOfPayment: | ||
| allOf: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. curious why we need all of these
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a workaround for an OpenAPI rule rather than anything semantic: a sibling key next to destination:
$ref: SweepRuleDestination.yaml
description: Where funds that settle into this account are swept.silently drops the description — the reader sees only whatever It is the house pattern rather than something I introduced — Worth noting this is a 3.0-era constraint and we are on 3.1, where |
||
| - $ref: ../quotes/PurposeOfPayment.yaml | ||
| description: The purpose of payment applied to each sweep. | ||
| example: SELF | ||
| description: | ||
| type: string | ||
| description: >- | ||
| Free-form description recorded on each sweep. Not delivered to the | ||
| recipient. | ||
| example: Rent sweep | ||
| remittanceInformation: | ||
| type: string | ||
| description: >- | ||
| Free-form information that travels with each sweep to the recipient. | ||
| example: Unit 4B March | ||
| platformFeeOverride: | ||
| allOf: | ||
| - $ref: ../quotes/PlatformFeeOverride.yaml | ||
| description: >- | ||
| Fee terms applied to every sweep this rule drives. Null when the | ||
| platform's configured fees apply. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| title: Sweep Rule Destination | ||
| type: object | ||
| description: >- | ||
| Where a rule-based account's credits are swept. | ||
| required: | ||
| - accountId | ||
| properties: | ||
| accountId: | ||
| type: string | ||
| description: The account that receives the swept funds. | ||
| example: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 | ||
| paymentRail: | ||
| allOf: | ||
| - $ref: ../common/PaymentRail.yaml | ||
| description: >- | ||
| The rail each sweep is sent over. Null when a rail is selected | ||
| automatically per sweep, in which case none is resolved ahead of time. | ||
| example: ACH |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| title: Sweep Rule Destination Request | ||
| type: object | ||
| description: >- | ||
| The account a rule-based account sweeps its incoming payments to. | ||
| additionalProperties: false | ||
| required: | ||
| - accountId | ||
| properties: | ||
| accountId: | ||
| type: string | ||
| description: >- | ||
| Reference to the account that receives the swept funds. May be an | ||
| external account or another internal account, but never a `RULE_BASED` | ||
| internal account — that account's own rule would sweep the funds on | ||
| again. The destination may be denominated in a different currency, in | ||
| which case the sweep is converted at the prevailing rate. | ||
| example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 | ||
| paymentRail: | ||
| description: >- | ||
| The payment rail to use when sweeping to an external account. Must be | ||
| one of the rails supported by the destination account. If omitted, a rail | ||
| is selected automatically for each sweep. Not accepted when the | ||
| destination is an internal account, which settles without a payment rail. | ||
| allOf: | ||
| - $ref: ../common/PaymentRail.yaml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| title: Sweep Rule Request | ||
| type: object | ||
| description: >- | ||
| The routing rule attached to a rule-based account. Every payment that settles | ||
| into the account is swept to the rule's destination, carrying the payment | ||
| metadata configured here. | ||
| additionalProperties: false | ||
| required: | ||
| - destination | ||
| properties: | ||
| destination: | ||
| allOf: | ||
| - $ref: ./SweepRuleDestinationRequest.yaml | ||
| description: Where funds that settle into this account are swept. | ||
| purposeOfPayment: | ||
| allOf: | ||
| - $ref: ../quotes/PurposeOfPayment.yaml | ||
| description: >- | ||
| The purpose of payment applied to each sweep. Required by some | ||
| destination geographies. | ||
| example: SELF | ||
| description: | ||
| type: string | ||
| maxLength: 255 | ||
| description: >- | ||
| Free-form description recorded on each sweep. Not delivered to the | ||
| recipient; use `remittanceInformation` for that. | ||
| example: Rent sweep | ||
| remittanceInformation: | ||
| type: string | ||
| maxLength: 1024 | ||
| description: >- | ||
| Free-form information that travels with each sweep to the recipient. | ||
| The field this populates depends on the payment rail: for ACH it | ||
| populates the Addenda record, for FedNow and RTP it populates the | ||
| remittanceInformation field, and for wires it populates the OBI | ||
| (Originator to Beneficiary Information) / beneficiary information. Only | ||
| printable ASCII characters are accepted, because the underlying rails | ||
| carry nothing else. | ||
| example: Unit 4B March |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| type: string | ||
| enum: | ||
| - BELOW_MINIMUM | ||
| - ABOVE_MAXIMUM | ||
| - NO_ELIGIBLE_RAIL | ||
| - QUOTE_FAILED | ||
| - EXECUTION_FAILED | ||
| description: | | ||
| Why the payment could not be swept to the rule's destination. | ||
|
|
||
| | Reason | Description | | ||
| |--------|-------------| | ||
| | `BELOW_MINIMUM` | The amount is below the smallest amount the corridor to the destination can carry. | | ||
| | `ABOVE_MAXIMUM` | The amount is above the largest amount the corridor to the destination can carry. | | ||
| | `NO_ELIGIBLE_RAIL` | No payment rail available to the destination could carry the sweep. | | ||
| | `QUOTE_FAILED` | Grid could not price the sweep to the destination. | | ||
| | `EXECUTION_FAILED` | The sweep was priced but the resulting payment did not go through. | |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.