From 26f0870f7f5457dfeabec34972812978879942da Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 14 Aug 2026 05:31:37 +0000 Subject: [PATCH 01/16] feat: add rule-based internal accounts and the sweep failure webhook --- mintlify/openapi.yaml | 498 ++++++++++++++++-- openapi.yaml | 498 ++++++++++++++++-- .../InternalAccountCreateRequest.yaml | 44 ++ .../customers/InternalAccountType.yaml | 6 + .../SweepRuleDestinationRequest.yaml | 25 + .../schemas/customers/SweepRuleRequest.yaml | 40 ++ .../schemas/webhooks/SweepFailure.yaml | 61 +++ .../schemas/webhooks/SweepFailureOutcome.yaml | 11 + .../schemas/webhooks/SweepFailureReason.yaml | 17 + .../schemas/webhooks/SweepWebhook.yaml | 12 + .../schemas/webhooks/WebhookType.yaml | 1 + openapi/openapi.yaml | 2 + .../customers_internal_accounts.yaml | 139 ++++- openapi/webhooks/sweep.yaml | 121 +++++ 14 files changed, 1365 insertions(+), 110 deletions(-) create mode 100644 openapi/components/schemas/customers/InternalAccountCreateRequest.yaml create mode 100644 openapi/components/schemas/customers/SweepRuleDestinationRequest.yaml create mode 100644 openapi/components/schemas/customers/SweepRuleRequest.yaml create mode 100644 openapi/components/schemas/webhooks/SweepFailure.yaml create mode 100644 openapi/components/schemas/webhooks/SweepFailureOutcome.yaml create mode 100644 openapi/components/schemas/webhooks/SweepFailureReason.yaml create mode 100644 openapi/components/schemas/webhooks/SweepWebhook.yaml create mode 100644 openapi/webhooks/sweep.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 15512c7a7..24a999189 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2092,6 +2092,125 @@ paths: schema: $ref: '#/components/schemas/Error500' /customers/internal-accounts: + post: + summary: Create a Customer internal account + description: | + Create a rule-based internal account for an existing customer. + + A rule-based account is an additional account number for a customer that + already holds an account in the same currency. It carries a routing rule: + every payment that settles into it is attributed to that account number and + then forwarded to the rule's destination, so you can issue one number per + payer and reconcile incoming payments without matching on remittance text. + + Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, + `INTERNAL_CRYPTO`, and `EMBEDDED_WALLET` accounts are provisioned + automatically when a customer is created or approved. + + The response carries the account's own `fundingPaymentInstructions` — the + routable number payers send to. Rule-based accounts are currently available + in `USD` for individual customers, and must be enabled for your platform + before this endpoint accepts requests. + + Creating an account mints a new account number that cannot be reversed, so + an `Idempotency-Key` header is required. A retry carrying the same key + returns the account created by the first request with a `200` instead of a + `201`; reusing a key for a materially different account is rejected with + `409`. + operationId: createCustomerInternalAccount + tags: + - Internal Accounts + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + description: | + A unique identifier for the request. Required, because creating the account mints an account number that cannot be reversed. Retries must carry the same key. + required: true + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountCreateRequest' + examples: + externalDestination: + summary: Forward every payment to an external account over ACH + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: RULE_BASED + currency: USD + label: invoice-4417 + sweepRule: + destination: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: ACH + purposeOfPayment: SELF + description: Rent forwarding + remittanceInformation: Unit 4B March + internalDestination: + summary: Forward every payment to another internal account + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: RULE_BASED + currency: USD + label: payer-northwind + sweepRule: + destination: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + responses: + '200': + description: The `Idempotency-Key` was already used to create this account. The account created by the first request is returned unchanged. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccount' + '201': + description: Internal account created. `fundingPaymentInstructions` carries the account's own number. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccount' + '400': + description: Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` is missing, when the currency is not supported for rule-based accounts, when the customer has no account in that currency yet, when the destination cannot accept the requested payment rail, and for general invalid parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - rule-based accounts are not enabled for this platform. + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Customer or destination account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Conflict - the `Idempotency-Key` was already used to create a different account, or the account for this key is still being provisioned. Retry with the same key. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' get: summary: List Customer internal accounts description: | @@ -2120,7 +2239,7 @@ paths: type: string - name: type in: query - description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts. + description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts, or `RULE_BASED` for the additional account numbers issued for a customer. required: false schema: $ref: '#/components/schemas/InternalAccountType' @@ -11890,6 +12009,106 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + sweep: + post: + summary: Sweep webhook + description: | + Webhook that is called when a payment settles into a rule-based internal account but cannot be forwarded to the destination configured on the account's rule. + + This endpoint should be implemented by clients of the Grid API. + + ### Authentication + + The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid. + To verify the signature: + 1. Get the Grid public key provided to you during integration + 2. Decode the base64 signature from the header + 3. Create a SHA-256 hash of the request body + 4. Verify the signature using the public key and the hash + + If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. + + ### Event types + + - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `outcome` says where the funds ended up: moved to the customer's account in the same currency, or still held in the rule-based account. A failed forward that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. + + Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `incomingTransactionId`. + operationId: sweepWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SweepWebhook' + examples: + movedToCanonicalAccount: + summary: The amount was below what the corridor carries, so it was moved to the customer's account + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000050 + type: SWEEP.FAILED + timestamp: '2026-05-09T10:00:00Z' + data: + sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: + amount: 250 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 + sendTransactionId: null + reason: BELOW_MINIMUM + outcome: MOVED_TO_CANONICAL_ACCOUNT + canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 + heldInRuleBasedAccount: + summary: The forward was sent but did not go through, and the funds stayed put + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000051 + type: SWEEP.FAILED + timestamp: '2026-05-09T10:05:00Z' + data: + sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: + amount: 12550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000012 + sendTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 + reason: EXECUTION_FAILED + outcome: HELD_IN_RULE_BASED_ACCOUNT + canonicalAccountId: null + responses: + '200': + description: | + Webhook received successfully + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized - Signature validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' components: securitySchemes: BasicAuth: @@ -14754,12 +14973,14 @@ components: - INTERNAL_FIAT - INTERNAL_CRYPTO - EMBEDDED_WALLET + - RULE_BASED description: |- Classification of an internal account. - `INTERNAL_FIAT`: A Grid-managed fiat holding account (for example, the USD holding account used as the source for Payouts flows). - `INTERNAL_CRYPTO`: A Grid-managed crypto holding account denominated in a stablecoin such as USDC. - `EMBEDDED_WALLET`: A self-custodial Embedded Wallet provisioned for the customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. + - `RULE_BASED`: 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. Created with `POST /customers/internal-accounts`. InternalAccountStatus: title: Internal Account Status type: string @@ -17579,6 +17800,134 @@ components: totalCount: type: integer description: Total number of customers matching the criteria (excluding pagination) + SweepRuleDestinationRequest: + title: Sweep Rule Destination Request + type: object + description: The account a rule-based account forwards its incoming payments to. + additionalProperties: false + required: + - accountId + properties: + accountId: + type: string + description: Reference to the account that receives the forwarded funds. May be an external account or another internal account, but never a `RULE_BASED` internal account — that account's own rule would forward the funds on again. The destination may be denominated in a different currency, in which case the forward is converted at the prevailing rate. + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: + description: The payment rail to use when forwarding to an external account. Must be one of the rails supported by the destination account. If omitted, a rail is selected automatically for each forward. Not accepted when the destination is an internal account, which settles without a payment rail. + allOf: + - $ref: '#/components/schemas/PaymentRail' + PurposeOfPayment: + type: string + description: The purpose of the payment. This may be required when sending to certain geographies (e.g. India). + enum: + - GIFT + - SELF + - GOODS_OR_SERVICES + - EDUCATION + - HEALTH_OR_MEDICAL + - REAL_ESTATE_PURCHASE + - TAX_PAYMENT + - LOAN_PAYMENT + - UTILITY_BILL + - DONATION + - TRAVEL + - FAMILY_SUPPORT + - SALARY_PAYMENT + - OTHER + SweepRuleRequest: + title: Sweep Rule Request + type: object + description: The routing rule attached to a rule-based account. Every payment that settles into the account is forwarded to the rule's destination, carrying the payment metadata configured here. + additionalProperties: false + required: + - destination + properties: + destination: + allOf: + - $ref: '#/components/schemas/SweepRuleDestinationRequest' + description: Where funds that settle into this account are forwarded. + purposeOfPayment: + allOf: + - $ref: '#/components/schemas/PurposeOfPayment' + description: The purpose of payment applied to each forward. Required by some destination geographies. + example: SELF + description: + type: string + maxLength: 255 + description: Free-form description recorded on each forward. Not delivered to the recipient; use `remittanceInformation` for that. + example: Rent forwarding + remittanceInformation: + type: string + maxLength: 1024 + description: 'Free-form information that travels with each forward 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 + InternalAccountCreateRequest: + title: Internal Account Create Request + type: object + description: Request body for `POST /customers/internal-accounts`. Only `RULE_BASED` accounts can be created directly, and they always carry a `sweepRule`. + additionalProperties: false + required: + - customerId + - type + - currency + properties: + customerId: + type: string + description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a forward cannot be completed. + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: + allOf: + - $ref: '#/components/schemas/InternalAccountType' + 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: '#/components/schemas/SweepRuleRequest' + description: The routing rule for the account. Required when `type` is `RULE_BASED`. + Error403: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 403 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | FORBIDDEN | Insufficient permissions | + | USER_NOT_READY | Customer exists but is not ready for operation | + | COUNTERPARTY_NOT_ALLOWED | Counterparty has not been enabled for your account | + | VELOCITY_LIMIT_EXCEEDED | Counterparty has exceeded velocity limits | + | END_USER_TERMS_NOT_ACCEPTED | Customer has not accepted the End User Terms | + enum: + - FORBIDDEN + - USER_NOT_READY + - COUNTERPARTY_NOT_ALLOWED + - VELOCITY_LIMIT_EXCEEDED + - END_USER_TERMS_NOT_ACCEPTED + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true PlatformInternalAccountListResponse: type: object required: @@ -23398,24 +23747,6 @@ components: description: The payment rail to use for the transfer. Must be one of the rails supported by the destination account. If not specified, the system will select a default rail. allOf: - $ref: '#/components/schemas/PaymentRail' - PurposeOfPayment: - type: string - description: The purpose of the payment. This may be required when sending to certain geographies (e.g. India). - enum: - - GIFT - - SELF - - GOODS_OR_SERVICES - - EDUCATION - - HEALTH_OR_MEDICAL - - REAL_ESTATE_PURCHASE - - TAX_PAYMENT - - LOAN_PAYMENT - - UTILITY_BILL - - DONATION - - TRAVEL - - FAMILY_SUPPORT - - SALARY_PAYMENT - - OTHER TransferOutRequest: type: object required: @@ -23892,41 +24223,6 @@ components: example: FULL_NAME: Jane Receiver NATIONALITY: FR - Error403: - type: object - required: - - message - - status - - code - properties: - status: - type: integer - enum: - - 403 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | FORBIDDEN | Insufficient permissions | - | USER_NOT_READY | Customer exists but is not ready for operation | - | COUNTERPARTY_NOT_ALLOWED | Counterparty has not been enabled for your account | - | VELOCITY_LIMIT_EXCEEDED | Counterparty has exceeded velocity limits | - | END_USER_TERMS_NOT_ACCEPTED | Customer has not accepted the End User Terms | - enum: - - FORBIDDEN - - USER_NOT_READY - - COUNTERPARTY_NOT_ALLOWED - - VELOCITY_LIMIT_EXCEEDED - - END_USER_TERMS_NOT_ACCEPTED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true ExecuteQuoteRequest: type: object description: Optional body for executing a quote. Only needed to request a specific Strong Customer Authentication factor (`scaFactor`) for the challenge this call issues; omit the body entirely otherwise. @@ -26763,6 +27059,7 @@ components: - CARD_TRANSACTION.EXCEPTION - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED + - SWEEP.FAILED - TEST description: Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot identifies the resource, the part after identifies the event. This lets consumers route purely on type without inspecting data.status. BaseWebhook: @@ -27151,6 +27448,99 @@ components: enum: - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED + SweepFailureReason: + type: string + enum: + - BELOW_MINIMUM + - ABOVE_MAXIMUM + - NO_ELIGIBLE_RAIL + - QUOTE_FAILED + - EXECUTION_FAILED + description: | + Why the payment could not be forwarded 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 forward. | + | `QUOTE_FAILED` | Grid could not price the forward to the destination. | + | `EXECUTION_FAILED` | The forward was priced but the resulting payment did not go through. | + SweepFailureOutcome: + type: string + enum: + - MOVED_TO_CANONICAL_ACCOUNT + - HELD_IN_RULE_BASED_ACCOUNT + description: | + Where the funds ended up after the forward failed. + + | Outcome | Description | + |---------|-------------| + | `MOVED_TO_CANONICAL_ACCOUNT` | The funds were moved to the customer's account in the same currency, identified by `canonicalAccountId`. | + | `HELD_IN_RULE_BASED_ACCOUNT` | The funds remain in the rule-based account. Grid retries the move when the next payment settles into the account. | + SweepFailure: + title: Sweep Failure + type: object + description: A payment that settled into a rule-based account but did not reach the destination of the account's rule. + required: + - sourceAccountId + - destinationAccountId + - amount + - incomingTransactionId + - reason + - outcome + properties: + sourceAccountId: + type: string + description: The rule-based internal account the payment settled into. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + destinationAccountId: + type: string + description: The destination configured on the account's rule, which the funds did not reach. + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: + allOf: + - $ref: '#/components/schemas/CurrencyAmount' + description: The amount that could not be forwarded. + incomingTransactionId: + type: string + description: The transaction that brought the funds into the rule-based account. Delivery of this event is at-least-once, so deduplicate on this value. + example: Transaction:019542f5-b3e7-1d02-0000-000000000010 + sendTransactionId: + type: + - string + - 'null' + description: The outgoing transaction created for the forward, when the forward got far enough to create one. Null when the forward was never attempted. Use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same forward rather than counting the failure twice. + example: Transaction:019542f5-b3e7-1d02-0000-000000000011 + reason: + allOf: + - $ref: '#/components/schemas/SweepFailureReason' + description: Why the forward could not be completed. + example: BELOW_MINIMUM + outcome: + allOf: + - $ref: '#/components/schemas/SweepFailureOutcome' + description: Where the funds ended up. + example: MOVED_TO_CANONICAL_ACCOUNT + canonicalAccountId: + type: + - string + - 'null' + description: The customer's account in the same currency that the funds were moved to. Null when `outcome` is `HELD_IN_RULE_BASED_ACCOUNT`. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 + SweepWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SweepFailure' + type: + type: string + enum: + - SWEEP.FAILED requestBodies: DocumentUploadRequestBody: required: true diff --git a/openapi.yaml b/openapi.yaml index 15512c7a7..24a999189 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2092,6 +2092,125 @@ paths: schema: $ref: '#/components/schemas/Error500' /customers/internal-accounts: + post: + summary: Create a Customer internal account + description: | + Create a rule-based internal account for an existing customer. + + A rule-based account is an additional account number for a customer that + already holds an account in the same currency. It carries a routing rule: + every payment that settles into it is attributed to that account number and + then forwarded to the rule's destination, so you can issue one number per + payer and reconcile incoming payments without matching on remittance text. + + Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, + `INTERNAL_CRYPTO`, and `EMBEDDED_WALLET` accounts are provisioned + automatically when a customer is created or approved. + + The response carries the account's own `fundingPaymentInstructions` — the + routable number payers send to. Rule-based accounts are currently available + in `USD` for individual customers, and must be enabled for your platform + before this endpoint accepts requests. + + Creating an account mints a new account number that cannot be reversed, so + an `Idempotency-Key` header is required. A retry carrying the same key + returns the account created by the first request with a `200` instead of a + `201`; reusing a key for a materially different account is rejected with + `409`. + operationId: createCustomerInternalAccount + tags: + - Internal Accounts + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + description: | + A unique identifier for the request. Required, because creating the account mints an account number that cannot be reversed. Retries must carry the same key. + required: true + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountCreateRequest' + examples: + externalDestination: + summary: Forward every payment to an external account over ACH + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: RULE_BASED + currency: USD + label: invoice-4417 + sweepRule: + destination: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: ACH + purposeOfPayment: SELF + description: Rent forwarding + remittanceInformation: Unit 4B March + internalDestination: + summary: Forward every payment to another internal account + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: RULE_BASED + currency: USD + label: payer-northwind + sweepRule: + destination: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + responses: + '200': + description: The `Idempotency-Key` was already used to create this account. The account created by the first request is returned unchanged. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccount' + '201': + description: Internal account created. `fundingPaymentInstructions` carries the account's own number. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccount' + '400': + description: Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` is missing, when the currency is not supported for rule-based accounts, when the customer has no account in that currency yet, when the destination cannot accept the requested payment rail, and for general invalid parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - rule-based accounts are not enabled for this platform. + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Customer or destination account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Conflict - the `Idempotency-Key` was already used to create a different account, or the account for this key is still being provisioned. Retry with the same key. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' get: summary: List Customer internal accounts description: | @@ -2120,7 +2239,7 @@ paths: type: string - name: type in: query - description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts. + description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts, or `RULE_BASED` for the additional account numbers issued for a customer. required: false schema: $ref: '#/components/schemas/InternalAccountType' @@ -11890,6 +12009,106 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + sweep: + post: + summary: Sweep webhook + description: | + Webhook that is called when a payment settles into a rule-based internal account but cannot be forwarded to the destination configured on the account's rule. + + This endpoint should be implemented by clients of the Grid API. + + ### Authentication + + The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid. + To verify the signature: + 1. Get the Grid public key provided to you during integration + 2. Decode the base64 signature from the header + 3. Create a SHA-256 hash of the request body + 4. Verify the signature using the public key and the hash + + If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. + + ### Event types + + - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `outcome` says where the funds ended up: moved to the customer's account in the same currency, or still held in the rule-based account. A failed forward that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. + + Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `incomingTransactionId`. + operationId: sweepWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SweepWebhook' + examples: + movedToCanonicalAccount: + summary: The amount was below what the corridor carries, so it was moved to the customer's account + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000050 + type: SWEEP.FAILED + timestamp: '2026-05-09T10:00:00Z' + data: + sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: + amount: 250 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 + sendTransactionId: null + reason: BELOW_MINIMUM + outcome: MOVED_TO_CANONICAL_ACCOUNT + canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 + heldInRuleBasedAccount: + summary: The forward was sent but did not go through, and the funds stayed put + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000051 + type: SWEEP.FAILED + timestamp: '2026-05-09T10:05:00Z' + data: + sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: + amount: 12550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000012 + sendTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 + reason: EXECUTION_FAILED + outcome: HELD_IN_RULE_BASED_ACCOUNT + canonicalAccountId: null + responses: + '200': + description: | + Webhook received successfully + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized - Signature validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' components: securitySchemes: BasicAuth: @@ -14754,12 +14973,14 @@ components: - INTERNAL_FIAT - INTERNAL_CRYPTO - EMBEDDED_WALLET + - RULE_BASED description: |- Classification of an internal account. - `INTERNAL_FIAT`: A Grid-managed fiat holding account (for example, the USD holding account used as the source for Payouts flows). - `INTERNAL_CRYPTO`: A Grid-managed crypto holding account denominated in a stablecoin such as USDC. - `EMBEDDED_WALLET`: A self-custodial Embedded Wallet provisioned for the customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. + - `RULE_BASED`: 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. Created with `POST /customers/internal-accounts`. InternalAccountStatus: title: Internal Account Status type: string @@ -17579,6 +17800,134 @@ components: totalCount: type: integer description: Total number of customers matching the criteria (excluding pagination) + SweepRuleDestinationRequest: + title: Sweep Rule Destination Request + type: object + description: The account a rule-based account forwards its incoming payments to. + additionalProperties: false + required: + - accountId + properties: + accountId: + type: string + description: Reference to the account that receives the forwarded funds. May be an external account or another internal account, but never a `RULE_BASED` internal account — that account's own rule would forward the funds on again. The destination may be denominated in a different currency, in which case the forward is converted at the prevailing rate. + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: + description: The payment rail to use when forwarding to an external account. Must be one of the rails supported by the destination account. If omitted, a rail is selected automatically for each forward. Not accepted when the destination is an internal account, which settles without a payment rail. + allOf: + - $ref: '#/components/schemas/PaymentRail' + PurposeOfPayment: + type: string + description: The purpose of the payment. This may be required when sending to certain geographies (e.g. India). + enum: + - GIFT + - SELF + - GOODS_OR_SERVICES + - EDUCATION + - HEALTH_OR_MEDICAL + - REAL_ESTATE_PURCHASE + - TAX_PAYMENT + - LOAN_PAYMENT + - UTILITY_BILL + - DONATION + - TRAVEL + - FAMILY_SUPPORT + - SALARY_PAYMENT + - OTHER + SweepRuleRequest: + title: Sweep Rule Request + type: object + description: The routing rule attached to a rule-based account. Every payment that settles into the account is forwarded to the rule's destination, carrying the payment metadata configured here. + additionalProperties: false + required: + - destination + properties: + destination: + allOf: + - $ref: '#/components/schemas/SweepRuleDestinationRequest' + description: Where funds that settle into this account are forwarded. + purposeOfPayment: + allOf: + - $ref: '#/components/schemas/PurposeOfPayment' + description: The purpose of payment applied to each forward. Required by some destination geographies. + example: SELF + description: + type: string + maxLength: 255 + description: Free-form description recorded on each forward. Not delivered to the recipient; use `remittanceInformation` for that. + example: Rent forwarding + remittanceInformation: + type: string + maxLength: 1024 + description: 'Free-form information that travels with each forward 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 + InternalAccountCreateRequest: + title: Internal Account Create Request + type: object + description: Request body for `POST /customers/internal-accounts`. Only `RULE_BASED` accounts can be created directly, and they always carry a `sweepRule`. + additionalProperties: false + required: + - customerId + - type + - currency + properties: + customerId: + type: string + description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a forward cannot be completed. + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: + allOf: + - $ref: '#/components/schemas/InternalAccountType' + 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: '#/components/schemas/SweepRuleRequest' + description: The routing rule for the account. Required when `type` is `RULE_BASED`. + Error403: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 403 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | FORBIDDEN | Insufficient permissions | + | USER_NOT_READY | Customer exists but is not ready for operation | + | COUNTERPARTY_NOT_ALLOWED | Counterparty has not been enabled for your account | + | VELOCITY_LIMIT_EXCEEDED | Counterparty has exceeded velocity limits | + | END_USER_TERMS_NOT_ACCEPTED | Customer has not accepted the End User Terms | + enum: + - FORBIDDEN + - USER_NOT_READY + - COUNTERPARTY_NOT_ALLOWED + - VELOCITY_LIMIT_EXCEEDED + - END_USER_TERMS_NOT_ACCEPTED + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true PlatformInternalAccountListResponse: type: object required: @@ -23398,24 +23747,6 @@ components: description: The payment rail to use for the transfer. Must be one of the rails supported by the destination account. If not specified, the system will select a default rail. allOf: - $ref: '#/components/schemas/PaymentRail' - PurposeOfPayment: - type: string - description: The purpose of the payment. This may be required when sending to certain geographies (e.g. India). - enum: - - GIFT - - SELF - - GOODS_OR_SERVICES - - EDUCATION - - HEALTH_OR_MEDICAL - - REAL_ESTATE_PURCHASE - - TAX_PAYMENT - - LOAN_PAYMENT - - UTILITY_BILL - - DONATION - - TRAVEL - - FAMILY_SUPPORT - - SALARY_PAYMENT - - OTHER TransferOutRequest: type: object required: @@ -23892,41 +24223,6 @@ components: example: FULL_NAME: Jane Receiver NATIONALITY: FR - Error403: - type: object - required: - - message - - status - - code - properties: - status: - type: integer - enum: - - 403 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | FORBIDDEN | Insufficient permissions | - | USER_NOT_READY | Customer exists but is not ready for operation | - | COUNTERPARTY_NOT_ALLOWED | Counterparty has not been enabled for your account | - | VELOCITY_LIMIT_EXCEEDED | Counterparty has exceeded velocity limits | - | END_USER_TERMS_NOT_ACCEPTED | Customer has not accepted the End User Terms | - enum: - - FORBIDDEN - - USER_NOT_READY - - COUNTERPARTY_NOT_ALLOWED - - VELOCITY_LIMIT_EXCEEDED - - END_USER_TERMS_NOT_ACCEPTED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true ExecuteQuoteRequest: type: object description: Optional body for executing a quote. Only needed to request a specific Strong Customer Authentication factor (`scaFactor`) for the challenge this call issues; omit the body entirely otherwise. @@ -26763,6 +27059,7 @@ components: - CARD_TRANSACTION.EXCEPTION - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED + - SWEEP.FAILED - TEST description: Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot identifies the resource, the part after identifies the event. This lets consumers route purely on type without inspecting data.status. BaseWebhook: @@ -27151,6 +27448,99 @@ components: enum: - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED + SweepFailureReason: + type: string + enum: + - BELOW_MINIMUM + - ABOVE_MAXIMUM + - NO_ELIGIBLE_RAIL + - QUOTE_FAILED + - EXECUTION_FAILED + description: | + Why the payment could not be forwarded 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 forward. | + | `QUOTE_FAILED` | Grid could not price the forward to the destination. | + | `EXECUTION_FAILED` | The forward was priced but the resulting payment did not go through. | + SweepFailureOutcome: + type: string + enum: + - MOVED_TO_CANONICAL_ACCOUNT + - HELD_IN_RULE_BASED_ACCOUNT + description: | + Where the funds ended up after the forward failed. + + | Outcome | Description | + |---------|-------------| + | `MOVED_TO_CANONICAL_ACCOUNT` | The funds were moved to the customer's account in the same currency, identified by `canonicalAccountId`. | + | `HELD_IN_RULE_BASED_ACCOUNT` | The funds remain in the rule-based account. Grid retries the move when the next payment settles into the account. | + SweepFailure: + title: Sweep Failure + type: object + description: A payment that settled into a rule-based account but did not reach the destination of the account's rule. + required: + - sourceAccountId + - destinationAccountId + - amount + - incomingTransactionId + - reason + - outcome + properties: + sourceAccountId: + type: string + description: The rule-based internal account the payment settled into. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + destinationAccountId: + type: string + description: The destination configured on the account's rule, which the funds did not reach. + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: + allOf: + - $ref: '#/components/schemas/CurrencyAmount' + description: The amount that could not be forwarded. + incomingTransactionId: + type: string + description: The transaction that brought the funds into the rule-based account. Delivery of this event is at-least-once, so deduplicate on this value. + example: Transaction:019542f5-b3e7-1d02-0000-000000000010 + sendTransactionId: + type: + - string + - 'null' + description: The outgoing transaction created for the forward, when the forward got far enough to create one. Null when the forward was never attempted. Use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same forward rather than counting the failure twice. + example: Transaction:019542f5-b3e7-1d02-0000-000000000011 + reason: + allOf: + - $ref: '#/components/schemas/SweepFailureReason' + description: Why the forward could not be completed. + example: BELOW_MINIMUM + outcome: + allOf: + - $ref: '#/components/schemas/SweepFailureOutcome' + description: Where the funds ended up. + example: MOVED_TO_CANONICAL_ACCOUNT + canonicalAccountId: + type: + - string + - 'null' + description: The customer's account in the same currency that the funds were moved to. Null when `outcome` is `HELD_IN_RULE_BASED_ACCOUNT`. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 + SweepWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/SweepFailure' + type: + type: string + enum: + - SWEEP.FAILED requestBodies: DocumentUploadRequestBody: required: true diff --git a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml new file mode 100644 index 000000000..da6cc5450 --- /dev/null +++ b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml @@ -0,0 +1,44 @@ +title: Internal Account Create Request +type: object +description: >- + Request body for `POST /customers/internal-accounts`. Only `RULE_BASED` + accounts can be created directly, and they always carry a `sweepRule`. +additionalProperties: false +required: + - customerId + - type + - currency +properties: + customerId: + type: string + description: >- + The customer the account is created for. The customer must already have a + verified account in the same currency, which is where funds land when a + forward cannot be completed. + 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`. diff --git a/openapi/components/schemas/customers/InternalAccountType.yaml b/openapi/components/schemas/customers/InternalAccountType.yaml index ce9959757..a9f3a7faf 100644 --- a/openapi/components/schemas/customers/InternalAccountType.yaml +++ b/openapi/components/schemas/customers/InternalAccountType.yaml @@ -4,6 +4,7 @@ enum: - INTERNAL_FIAT - INTERNAL_CRYPTO - EMBEDDED_WALLET + - RULE_BASED description: >- Classification of an internal account. @@ -17,3 +18,8 @@ description: >- - `EMBEDDED_WALLET`: A self-custodial Embedded Wallet provisioned for the customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. + + - `RULE_BASED`: 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. Created with + `POST /customers/internal-accounts`. diff --git a/openapi/components/schemas/customers/SweepRuleDestinationRequest.yaml b/openapi/components/schemas/customers/SweepRuleDestinationRequest.yaml new file mode 100644 index 000000000..f3ee871b6 --- /dev/null +++ b/openapi/components/schemas/customers/SweepRuleDestinationRequest.yaml @@ -0,0 +1,25 @@ +title: Sweep Rule Destination Request +type: object +description: >- + The account a rule-based account forwards its incoming payments to. +additionalProperties: false +required: + - accountId +properties: + accountId: + type: string + description: >- + Reference to the account that receives the forwarded funds. May be an + external account or another internal account, but never a `RULE_BASED` + internal account — that account's own rule would forward the funds on + again. The destination may be denominated in a different currency, in + which case the forward is converted at the prevailing rate. + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: + description: >- + The payment rail to use when forwarding to an external account. Must be + one of the rails supported by the destination account. If omitted, a rail + is selected automatically for each forward. Not accepted when the + destination is an internal account, which settles without a payment rail. + allOf: + - $ref: ../common/PaymentRail.yaml diff --git a/openapi/components/schemas/customers/SweepRuleRequest.yaml b/openapi/components/schemas/customers/SweepRuleRequest.yaml new file mode 100644 index 000000000..081ec1818 --- /dev/null +++ b/openapi/components/schemas/customers/SweepRuleRequest.yaml @@ -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 forwarded 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 forwarded. + purposeOfPayment: + allOf: + - $ref: ../quotes/PurposeOfPayment.yaml + description: >- + The purpose of payment applied to each forward. Required by some + destination geographies. + example: SELF + description: + type: string + maxLength: 255 + description: >- + Free-form description recorded on each forward. Not delivered to the + recipient; use `remittanceInformation` for that. + example: Rent forwarding + remittanceInformation: + type: string + maxLength: 1024 + description: >- + Free-form information that travels with each forward 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 diff --git a/openapi/components/schemas/webhooks/SweepFailure.yaml b/openapi/components/schemas/webhooks/SweepFailure.yaml new file mode 100644 index 000000000..d1f024bfe --- /dev/null +++ b/openapi/components/schemas/webhooks/SweepFailure.yaml @@ -0,0 +1,61 @@ +title: Sweep Failure +type: object +description: >- + A payment that settled into a rule-based account but did not reach the + destination of the account's rule. +required: + - sourceAccountId + - destinationAccountId + - amount + - incomingTransactionId + - reason + - outcome +properties: + sourceAccountId: + type: string + description: The rule-based internal account the payment settled into. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + destinationAccountId: + type: string + description: >- + The destination configured on the account's rule, which the funds did not + reach. + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: + allOf: + - $ref: ../common/CurrencyAmount.yaml + description: The amount that could not be forwarded. + incomingTransactionId: + type: string + description: >- + The transaction that brought the funds into the rule-based account. + Delivery of this event is at-least-once, so deduplicate on this value. + example: Transaction:019542f5-b3e7-1d02-0000-000000000010 + sendTransactionId: + type: + - string + - 'null' + description: >- + The outgoing transaction created for the forward, when the forward got + far enough to create one. Null when the forward was never attempted. Use + it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for + the same forward rather than counting the failure twice. + example: Transaction:019542f5-b3e7-1d02-0000-000000000011 + reason: + allOf: + - $ref: ./SweepFailureReason.yaml + description: Why the forward could not be completed. + example: BELOW_MINIMUM + outcome: + allOf: + - $ref: ./SweepFailureOutcome.yaml + description: Where the funds ended up. + example: MOVED_TO_CANONICAL_ACCOUNT + canonicalAccountId: + type: + - string + - 'null' + description: >- + The customer's account in the same currency that the funds were moved to. + Null when `outcome` is `HELD_IN_RULE_BASED_ACCOUNT`. + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 diff --git a/openapi/components/schemas/webhooks/SweepFailureOutcome.yaml b/openapi/components/schemas/webhooks/SweepFailureOutcome.yaml new file mode 100644 index 000000000..4064b3cb8 --- /dev/null +++ b/openapi/components/schemas/webhooks/SweepFailureOutcome.yaml @@ -0,0 +1,11 @@ +type: string +enum: + - MOVED_TO_CANONICAL_ACCOUNT + - HELD_IN_RULE_BASED_ACCOUNT +description: | + Where the funds ended up after the forward failed. + + | Outcome | Description | + |---------|-------------| + | `MOVED_TO_CANONICAL_ACCOUNT` | The funds were moved to the customer's account in the same currency, identified by `canonicalAccountId`. | + | `HELD_IN_RULE_BASED_ACCOUNT` | The funds remain in the rule-based account. Grid retries the move when the next payment settles into the account. | diff --git a/openapi/components/schemas/webhooks/SweepFailureReason.yaml b/openapi/components/schemas/webhooks/SweepFailureReason.yaml new file mode 100644 index 000000000..d2c69b368 --- /dev/null +++ b/openapi/components/schemas/webhooks/SweepFailureReason.yaml @@ -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 forwarded 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 forward. | + | `QUOTE_FAILED` | Grid could not price the forward to the destination. | + | `EXECUTION_FAILED` | The forward was priced but the resulting payment did not go through. | diff --git a/openapi/components/schemas/webhooks/SweepWebhook.yaml b/openapi/components/schemas/webhooks/SweepWebhook.yaml new file mode 100644 index 000000000..4b7df27d8 --- /dev/null +++ b/openapi/components/schemas/webhooks/SweepWebhook.yaml @@ -0,0 +1,12 @@ +allOf: + - $ref: ./BaseWebhook.yaml + - type: object + required: + - data + properties: + data: + $ref: ./SweepFailure.yaml + type: + type: string + enum: + - SWEEP.FAILED diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml index 0d7da939d..2e3430531 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -45,6 +45,7 @@ enum: - CARD_TRANSACTION.EXCEPTION - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED + - SWEEP.FAILED - TEST description: >- Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 52d6d4fad..81604d560 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -413,6 +413,8 @@ webhooks: $ref: webhooks/card-transaction.yaml wallet-operation: $ref: webhooks/wallet-operation.yaml + sweep: + $ref: webhooks/sweep.yaml security: - BasicAuth: [] - AgentAuth: [] diff --git a/openapi/paths/customers/customers_internal_accounts.yaml b/openapi/paths/customers/customers_internal_accounts.yaml index 123247378..1be79f610 100644 --- a/openapi/paths/customers/customers_internal_accounts.yaml +++ b/openapi/paths/customers/customers_internal_accounts.yaml @@ -1,3 +1,137 @@ +post: + summary: Create a Customer internal account + description: | + Create a rule-based internal account for an existing customer. + + A rule-based account is an additional account number for a customer that + already holds an account in the same currency. It carries a routing rule: + every payment that settles into it is attributed to that account number and + then forwarded to the rule's destination, so you can issue one number per + payer and reconcile incoming payments without matching on remittance text. + + Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, + `INTERNAL_CRYPTO`, and `EMBEDDED_WALLET` accounts are provisioned + automatically when a customer is created or approved. + + The response carries the account's own `fundingPaymentInstructions` — the + routable number payers send to. Rule-based accounts are currently available + in `USD` for individual customers, and must be enabled for your platform + before this endpoint accepts requests. + + Creating an account mints a new account number that cannot be reversed, so + an `Idempotency-Key` header is required. A retry carrying the same key + returns the account created by the first request with a `200` instead of a + `201`; reusing a key for a materially different account is rejected with + `409`. + operationId: createCustomerInternalAccount + tags: + - Internal Accounts + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + description: > + A unique identifier for the request. Required, because creating the + account mints an account number that cannot be reversed. Retries must + carry the same key. + required: true + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/customers/InternalAccountCreateRequest.yaml + examples: + externalDestination: + summary: Forward every payment to an external account over ACH + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: RULE_BASED + currency: USD + label: invoice-4417 + sweepRule: + destination: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: ACH + purposeOfPayment: SELF + description: Rent forwarding + remittanceInformation: Unit 4B March + internalDestination: + summary: Forward every payment to another internal account + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: RULE_BASED + currency: USD + label: payer-northwind + sweepRule: + destination: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + responses: + '200': + description: >- + The `Idempotency-Key` was already used to create this account. The + account created by the first request is returned unchanged. + content: + application/json: + schema: + $ref: ../../components/schemas/customers/InternalAccount.yaml + '201': + description: >- + Internal account created. `fundingPaymentInstructions` carries the + account's own number. + content: + application/json: + schema: + $ref: ../../components/schemas/customers/InternalAccount.yaml + '400': + description: >- + Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` + is missing, when the currency is not supported for rule-based accounts, + when the customer has no account in that currency yet, when the + destination cannot accept the requested payment rail, and for general + invalid parameters. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '403': + description: >- + Forbidden - rule-based accounts are not enabled for this platform. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error403.yaml + '404': + description: Customer or destination account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + Conflict - the `Idempotency-Key` was already used to create a different + account, or the account for this key is still being provisioned. Retry + with the same key. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml get: summary: List Customer internal accounts description: | @@ -28,8 +162,9 @@ get: in: query description: >- Filter by internal account type. Use `EMBEDDED_WALLET` to find the - self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / - `INTERNAL_CRYPTO` for the platform-managed holding accounts. + self-custodial wallet provisioned for a customer, `INTERNAL_FIAT` / + `INTERNAL_CRYPTO` for the platform-managed holding accounts, or + `RULE_BASED` for the additional account numbers issued for a customer. required: false schema: $ref: ../../components/schemas/customers/InternalAccountType.yaml diff --git a/openapi/webhooks/sweep.yaml b/openapi/webhooks/sweep.yaml new file mode 100644 index 000000000..e386f445b --- /dev/null +++ b/openapi/webhooks/sweep.yaml @@ -0,0 +1,121 @@ +post: + summary: Sweep webhook + description: > + Webhook that is called when a payment settles into a rule-based internal + account but cannot be forwarded to the destination configured on the + account's rule. + + + This endpoint should be implemented by clients of the Grid API. + + + ### Authentication + + + The webhook includes a signature in the `X-Grid-Signature` header that + allows you to verify that the webhook was sent by Grid. + + To verify the signature: + + 1. Get the Grid public key provided to you during integration + + 2. Decode the base64 signature from the header + + 3. Create a SHA-256 hash of the request body + + 4. Verify the signature using the public key and the hash + + + If the signature verification succeeds, the webhook is authentic. If not, it + should be rejected. + + + ### Event types + + + - `SWEEP.FAILED` — Fired when the funds do not reach the rule's + destination. `reason` says why, and `outcome` says where the funds ended up: + moved to the customer's account in the same currency, or still held in the + rule-based account. A failed forward that also created an outgoing payment + fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on + `sendTransactionId` rather than counting the failure twice. + + + Delivery is at-least-once, so the same failure can arrive more than once + with different webhook ids. Deduplicate on `incomingTransactionId`. + operationId: sweepWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/webhooks/SweepWebhook.yaml + examples: + movedToCanonicalAccount: + summary: The amount was below what the corridor carries, so it was moved to the customer's account + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000050 + type: SWEEP.FAILED + timestamp: '2026-05-09T10:00:00Z' + data: + sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: + amount: 250 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 + sendTransactionId: null + reason: BELOW_MINIMUM + outcome: MOVED_TO_CANONICAL_ACCOUNT + canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 + heldInRuleBasedAccount: + summary: The forward was sent but did not go through, and the funds stayed put + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000051 + type: SWEEP.FAILED + timestamp: '2026-05-09T10:05:00Z' + data: + sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 + destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: + amount: 12550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000012 + sendTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 + reason: EXECUTION_FAILED + outcome: HELD_IN_RULE_BASED_ACCOUNT + canonicalAccountId: null + responses: + '200': + description: > + Webhook received successfully + '400': + description: Bad request + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized - Signature validation failed + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error401.yaml + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error409.yaml From 32633328888158d6753bec9c70155600bdcc0eda Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 2 Sep 2026 00:09:06 +0000 Subject: [PATCH 02/16] feat: create internal accounts at POST /internal-accounts 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. --- mintlify/openapi.yaml | 338 +++++++++++------- openapi.yaml | 338 +++++++++++------- .../schemas/customers/InternalAccount.yaml | 13 + .../InternalAccountCreateRequest.yaml | 2 +- .../customers/InternalAccountType.yaml | 2 +- .../schemas/customers/SweepRule.yaml | 52 +++ .../customers/SweepRuleDestination.yaml | 18 + openapi/openapi.yaml | 2 + .../customers_internal_accounts.yaml | 134 ------- openapi/paths/internal_accounts.yaml | 134 +++++++ 10 files changed, 621 insertions(+), 412 deletions(-) create mode 100644 openapi/components/schemas/customers/SweepRule.yaml create mode 100644 openapi/components/schemas/customers/SweepRuleDestination.yaml create mode 100644 openapi/paths/internal_accounts.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 24a999189..3179d979d 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2091,9 +2091,9 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/internal-accounts: + /internal-accounts: post: - summary: Create a Customer internal account + summary: Create an internal account description: | Create a rule-based internal account for an existing customer. @@ -2117,7 +2117,7 @@ paths: returns the account created by the first request with a `200` instead of a `201`; reusing a key for a materially different account is rejected with `409`. - operationId: createCustomerInternalAccount + operationId: createInternalAccount tags: - Internal Accounts security: @@ -2211,6 +2211,7 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/internal-accounts: get: summary: List Customer internal accounts description: | @@ -14980,7 +14981,100 @@ components: - `INTERNAL_FIAT`: A Grid-managed fiat holding account (for example, the USD holding account used as the source for Payouts flows). - `INTERNAL_CRYPTO`: A Grid-managed crypto holding account denominated in a stablecoin such as USDC. - `EMBEDDED_WALLET`: A self-custodial Embedded Wallet provisioned for the customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. - - `RULE_BASED`: 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. Created with `POST /customers/internal-accounts`. + - `RULE_BASED`: 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. Created with `POST /internal-accounts`. + SweepRuleDestinationRequest: + title: Sweep Rule Destination Request + type: object + description: The account a rule-based account forwards its incoming payments to. + additionalProperties: false + required: + - accountId + properties: + accountId: + type: string + description: Reference to the account that receives the forwarded funds. May be an external account or another internal account, but never a `RULE_BASED` internal account — that account's own rule would forward the funds on again. The destination may be denominated in a different currency, in which case the forward is converted at the prevailing rate. + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: + description: The payment rail to use when forwarding to an external account. Must be one of the rails supported by the destination account. If omitted, a rail is selected automatically for each forward. Not accepted when the destination is an internal account, which settles without a payment rail. + allOf: + - $ref: '#/components/schemas/PaymentRail' + PurposeOfPayment: + type: string + description: The purpose of the payment. This may be required when sending to certain geographies (e.g. India). + enum: + - GIFT + - SELF + - GOODS_OR_SERVICES + - EDUCATION + - HEALTH_OR_MEDICAL + - REAL_ESTATE_PURCHASE + - TAX_PAYMENT + - LOAN_PAYMENT + - UTILITY_BILL + - DONATION + - TRAVEL + - FAMILY_SUPPORT + - SALARY_PAYMENT + - OTHER + SweepRuleRequest: + title: Sweep Rule Request + type: object + description: The routing rule attached to a rule-based account. Every payment that settles into the account is forwarded to the rule's destination, carrying the payment metadata configured here. + additionalProperties: false + required: + - destination + properties: + destination: + allOf: + - $ref: '#/components/schemas/SweepRuleDestinationRequest' + description: Where funds that settle into this account are forwarded. + purposeOfPayment: + allOf: + - $ref: '#/components/schemas/PurposeOfPayment' + description: The purpose of payment applied to each forward. Required by some destination geographies. + example: SELF + description: + type: string + maxLength: 255 + description: Free-form description recorded on each forward. Not delivered to the recipient; use `remittanceInformation` for that. + example: Rent forwarding + remittanceInformation: + type: string + maxLength: 1024 + description: 'Free-form information that travels with each forward 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 + InternalAccountCreateRequest: + 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: + - customerId + - type + - currency + properties: + customerId: + type: string + description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a forward cannot be completed. + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: + allOf: + - $ref: '#/components/schemas/InternalAccountType' + 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: '#/components/schemas/SweepRuleRequest' + description: The routing rule for the account. Required when `type` is `RULE_BASED`. InternalAccountStatus: title: Internal Account Status type: string @@ -17731,6 +17825,79 @@ components: SWIFT_ACCOUNT: '#/components/schemas/PaymentSwiftAccountInfo' CNY_ACCOUNT: '#/components/schemas/PaymentCnyAccountInfo' ILS_ACCOUNT: '#/components/schemas/PaymentIlsAccountInfo' + SweepRuleDestination: + title: Sweep Rule Destination + type: object + description: Where a rule-based account's credits are forwarded. + required: + - accountId + properties: + accountId: + type: string + description: The account that receives the forwarded funds. + example: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + paymentRail: + allOf: + - $ref: '#/components/schemas/PaymentRail' + description: The rail each forward is sent over. Null when a rail is selected automatically per forward, in which case none is resolved ahead of time. + example: ACH + PlatformFeeOverride: + type: object + description: Overrides the platform-collected fee for this transaction. When present, it replaces any configured platform-collected fees that would otherwise apply to the transaction. Currently only supported when the quote's source currency is USD; the fixed fee must be denominated in the source currency. + required: + - platformFixedFee + - platformVariableFeeBps + properties: + platformFixedFee: + $ref: '#/components/schemas/FixedFee' + description: Fixed fee charged for this transaction. Must be denominated in the quote's source currency (USD today). + platformVariableFeeBps: + type: integer + minimum: 0 + maximum: 10000 + description: Variable fee in basis points (1 bps = 0.01%) to apply to the transaction's source-currency amount. + example: 30 + example: + platformFixedFee: + amount: 50 + currency: USD + platformVariableFeeBps: 30 + SweepRule: + 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: '#/components/schemas/SweepRuleDestination' + description: Where funds that settle into this account are forwarded. + minimumAmount: + allOf: + - $ref: '#/components/schemas/CurrencyAmount' + 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 forwarded. Zero means no floor applies, which is the case for a 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: '#/components/schemas/CurrencyAmount' + description: The largest balance the corridor to the destination can carry. Null means no ceiling applies. A settled balance above it is not forwarded. + purposeOfPayment: + allOf: + - $ref: '#/components/schemas/PurposeOfPayment' + description: The purpose of payment applied to each forward. + example: SELF + description: + type: string + description: Free-form description recorded on each forward. Not delivered to the recipient. + example: Rent forwarding + remittanceInformation: + type: string + description: Free-form information that travels with each forward to the recipient. + example: Unit 4B March + platformFeeOverride: + allOf: + - $ref: '#/components/schemas/PlatformFeeOverride' + description: Fee terms applied to every forward this rule drives. Null when the platform's configured fees apply. InternalAccount: type: object required: @@ -17766,6 +17933,15 @@ components: description: Payment instructions for funding the account items: $ref: '#/components/schemas/PaymentInstructions' + label: + type: string + maxLength: 255 + description: The platform-supplied label recorded when the account was created. Null for accounts that carry none. + example: invoice-4417 + sweepRule: + allOf: + - $ref: '#/components/schemas/SweepRule' + description: The routing rule attached to this account. Null for accounts that carry no rule, which is every account other than a `RULE_BASED` one. privateEnabled: type: boolean description: Whether wallet privacy is enabled for the Embedded Wallet. Only present for `EMBEDDED_WALLET` internal accounts. @@ -17780,119 +17956,6 @@ components: format: date-time description: Timestamp when the internal account was last updated example: '2025-10-03T12:30:00Z' - InternalAccountListResponse: - type: object - required: - - data - - hasMore - properties: - data: - type: array - description: List of internal accounts matching the filter criteria - items: - $ref: '#/components/schemas/InternalAccount' - hasMore: - type: boolean - description: Indicates if more results are available beyond this page - nextCursor: - type: string - description: Cursor to retrieve the next page of results (only present if hasMore is true) - totalCount: - type: integer - description: Total number of customers matching the criteria (excluding pagination) - SweepRuleDestinationRequest: - title: Sweep Rule Destination Request - type: object - description: The account a rule-based account forwards its incoming payments to. - additionalProperties: false - required: - - accountId - properties: - accountId: - type: string - description: Reference to the account that receives the forwarded funds. May be an external account or another internal account, but never a `RULE_BASED` internal account — that account's own rule would forward the funds on again. The destination may be denominated in a different currency, in which case the forward is converted at the prevailing rate. - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - paymentRail: - description: The payment rail to use when forwarding to an external account. Must be one of the rails supported by the destination account. If omitted, a rail is selected automatically for each forward. Not accepted when the destination is an internal account, which settles without a payment rail. - allOf: - - $ref: '#/components/schemas/PaymentRail' - PurposeOfPayment: - type: string - description: The purpose of the payment. This may be required when sending to certain geographies (e.g. India). - enum: - - GIFT - - SELF - - GOODS_OR_SERVICES - - EDUCATION - - HEALTH_OR_MEDICAL - - REAL_ESTATE_PURCHASE - - TAX_PAYMENT - - LOAN_PAYMENT - - UTILITY_BILL - - DONATION - - TRAVEL - - FAMILY_SUPPORT - - SALARY_PAYMENT - - OTHER - SweepRuleRequest: - title: Sweep Rule Request - type: object - description: The routing rule attached to a rule-based account. Every payment that settles into the account is forwarded to the rule's destination, carrying the payment metadata configured here. - additionalProperties: false - required: - - destination - properties: - destination: - allOf: - - $ref: '#/components/schemas/SweepRuleDestinationRequest' - description: Where funds that settle into this account are forwarded. - purposeOfPayment: - allOf: - - $ref: '#/components/schemas/PurposeOfPayment' - description: The purpose of payment applied to each forward. Required by some destination geographies. - example: SELF - description: - type: string - maxLength: 255 - description: Free-form description recorded on each forward. Not delivered to the recipient; use `remittanceInformation` for that. - example: Rent forwarding - remittanceInformation: - type: string - maxLength: 1024 - description: 'Free-form information that travels with each forward 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 - InternalAccountCreateRequest: - title: Internal Account Create Request - type: object - description: Request body for `POST /customers/internal-accounts`. Only `RULE_BASED` accounts can be created directly, and they always carry a `sweepRule`. - additionalProperties: false - required: - - customerId - - type - - currency - properties: - customerId: - type: string - description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a forward cannot be completed. - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: - allOf: - - $ref: '#/components/schemas/InternalAccountType' - 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: '#/components/schemas/SweepRuleRequest' - description: The routing rule for the account. Required when `type` is `RULE_BASED`. Error403: type: object required: @@ -17928,6 +17991,26 @@ components: type: object description: Additional error details additionalProperties: true + InternalAccountListResponse: + type: object + required: + - data + - hasMore + properties: + data: + type: array + description: List of internal accounts matching the filter criteria + items: + $ref: '#/components/schemas/InternalAccount' + hasMore: + type: boolean + description: Indicates if more results are available beyond this page + nextCursor: + type: string + description: Cursor to retrieve the next page of results (only present if hasMore is true) + totalCount: + type: integer + description: Total number of customers matching the criteria (excluding pagination) PlatformInternalAccountListResponse: type: object required: @@ -24143,27 +24226,6 @@ components: - SENDING - RECEIVING description: The side of the quote which should be locked and specified in the `lockedCurrencyAmount`. For example, if I want to send exactly $5 MXN from my wallet, I would set this to "sending", and the `lockedCurrencyAmount` to 500 (in cents). If I want the receiver to receive exactly $10 USD, I would set this to "receiving" and the `lockedCurrencyAmount` to 10000 (in cents). - PlatformFeeOverride: - type: object - description: Overrides the platform-collected fee for this transaction. When present, it replaces any configured platform-collected fees that would otherwise apply to the transaction. Currently only supported when the quote's source currency is USD; the fixed fee must be denominated in the source currency. - required: - - platformFixedFee - - platformVariableFeeBps - properties: - platformFixedFee: - $ref: '#/components/schemas/FixedFee' - description: Fixed fee charged for this transaction. Must be denominated in the quote's source currency (USD today). - platformVariableFeeBps: - type: integer - minimum: 0 - maximum: 10000 - description: Variable fee in basis points (1 bps = 0.01%) to apply to the transaction's source-currency amount. - example: 30 - example: - platformFixedFee: - amount: 50 - currency: USD - platformVariableFeeBps: 30 QuoteRequest: type: object required: diff --git a/openapi.yaml b/openapi.yaml index 24a999189..3179d979d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2091,9 +2091,9 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/internal-accounts: + /internal-accounts: post: - summary: Create a Customer internal account + summary: Create an internal account description: | Create a rule-based internal account for an existing customer. @@ -2117,7 +2117,7 @@ paths: returns the account created by the first request with a `200` instead of a `201`; reusing a key for a materially different account is rejected with `409`. - operationId: createCustomerInternalAccount + operationId: createInternalAccount tags: - Internal Accounts security: @@ -2211,6 +2211,7 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/internal-accounts: get: summary: List Customer internal accounts description: | @@ -14980,7 +14981,100 @@ components: - `INTERNAL_FIAT`: A Grid-managed fiat holding account (for example, the USD holding account used as the source for Payouts flows). - `INTERNAL_CRYPTO`: A Grid-managed crypto holding account denominated in a stablecoin such as USDC. - `EMBEDDED_WALLET`: A self-custodial Embedded Wallet provisioned for the customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. - - `RULE_BASED`: 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. Created with `POST /customers/internal-accounts`. + - `RULE_BASED`: 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. Created with `POST /internal-accounts`. + SweepRuleDestinationRequest: + title: Sweep Rule Destination Request + type: object + description: The account a rule-based account forwards its incoming payments to. + additionalProperties: false + required: + - accountId + properties: + accountId: + type: string + description: Reference to the account that receives the forwarded funds. May be an external account or another internal account, but never a `RULE_BASED` internal account — that account's own rule would forward the funds on again. The destination may be denominated in a different currency, in which case the forward is converted at the prevailing rate. + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: + description: The payment rail to use when forwarding to an external account. Must be one of the rails supported by the destination account. If omitted, a rail is selected automatically for each forward. Not accepted when the destination is an internal account, which settles without a payment rail. + allOf: + - $ref: '#/components/schemas/PaymentRail' + PurposeOfPayment: + type: string + description: The purpose of the payment. This may be required when sending to certain geographies (e.g. India). + enum: + - GIFT + - SELF + - GOODS_OR_SERVICES + - EDUCATION + - HEALTH_OR_MEDICAL + - REAL_ESTATE_PURCHASE + - TAX_PAYMENT + - LOAN_PAYMENT + - UTILITY_BILL + - DONATION + - TRAVEL + - FAMILY_SUPPORT + - SALARY_PAYMENT + - OTHER + SweepRuleRequest: + title: Sweep Rule Request + type: object + description: The routing rule attached to a rule-based account. Every payment that settles into the account is forwarded to the rule's destination, carrying the payment metadata configured here. + additionalProperties: false + required: + - destination + properties: + destination: + allOf: + - $ref: '#/components/schemas/SweepRuleDestinationRequest' + description: Where funds that settle into this account are forwarded. + purposeOfPayment: + allOf: + - $ref: '#/components/schemas/PurposeOfPayment' + description: The purpose of payment applied to each forward. Required by some destination geographies. + example: SELF + description: + type: string + maxLength: 255 + description: Free-form description recorded on each forward. Not delivered to the recipient; use `remittanceInformation` for that. + example: Rent forwarding + remittanceInformation: + type: string + maxLength: 1024 + description: 'Free-form information that travels with each forward 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 + InternalAccountCreateRequest: + 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: + - customerId + - type + - currency + properties: + customerId: + type: string + description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a forward cannot be completed. + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: + allOf: + - $ref: '#/components/schemas/InternalAccountType' + 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: '#/components/schemas/SweepRuleRequest' + description: The routing rule for the account. Required when `type` is `RULE_BASED`. InternalAccountStatus: title: Internal Account Status type: string @@ -17731,6 +17825,79 @@ components: SWIFT_ACCOUNT: '#/components/schemas/PaymentSwiftAccountInfo' CNY_ACCOUNT: '#/components/schemas/PaymentCnyAccountInfo' ILS_ACCOUNT: '#/components/schemas/PaymentIlsAccountInfo' + SweepRuleDestination: + title: Sweep Rule Destination + type: object + description: Where a rule-based account's credits are forwarded. + required: + - accountId + properties: + accountId: + type: string + description: The account that receives the forwarded funds. + example: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + paymentRail: + allOf: + - $ref: '#/components/schemas/PaymentRail' + description: The rail each forward is sent over. Null when a rail is selected automatically per forward, in which case none is resolved ahead of time. + example: ACH + PlatformFeeOverride: + type: object + description: Overrides the platform-collected fee for this transaction. When present, it replaces any configured platform-collected fees that would otherwise apply to the transaction. Currently only supported when the quote's source currency is USD; the fixed fee must be denominated in the source currency. + required: + - platformFixedFee + - platformVariableFeeBps + properties: + platformFixedFee: + $ref: '#/components/schemas/FixedFee' + description: Fixed fee charged for this transaction. Must be denominated in the quote's source currency (USD today). + platformVariableFeeBps: + type: integer + minimum: 0 + maximum: 10000 + description: Variable fee in basis points (1 bps = 0.01%) to apply to the transaction's source-currency amount. + example: 30 + example: + platformFixedFee: + amount: 50 + currency: USD + platformVariableFeeBps: 30 + SweepRule: + 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: '#/components/schemas/SweepRuleDestination' + description: Where funds that settle into this account are forwarded. + minimumAmount: + allOf: + - $ref: '#/components/schemas/CurrencyAmount' + 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 forwarded. Zero means no floor applies, which is the case for a 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: '#/components/schemas/CurrencyAmount' + description: The largest balance the corridor to the destination can carry. Null means no ceiling applies. A settled balance above it is not forwarded. + purposeOfPayment: + allOf: + - $ref: '#/components/schemas/PurposeOfPayment' + description: The purpose of payment applied to each forward. + example: SELF + description: + type: string + description: Free-form description recorded on each forward. Not delivered to the recipient. + example: Rent forwarding + remittanceInformation: + type: string + description: Free-form information that travels with each forward to the recipient. + example: Unit 4B March + platformFeeOverride: + allOf: + - $ref: '#/components/schemas/PlatformFeeOverride' + description: Fee terms applied to every forward this rule drives. Null when the platform's configured fees apply. InternalAccount: type: object required: @@ -17766,6 +17933,15 @@ components: description: Payment instructions for funding the account items: $ref: '#/components/schemas/PaymentInstructions' + label: + type: string + maxLength: 255 + description: The platform-supplied label recorded when the account was created. Null for accounts that carry none. + example: invoice-4417 + sweepRule: + allOf: + - $ref: '#/components/schemas/SweepRule' + description: The routing rule attached to this account. Null for accounts that carry no rule, which is every account other than a `RULE_BASED` one. privateEnabled: type: boolean description: Whether wallet privacy is enabled for the Embedded Wallet. Only present for `EMBEDDED_WALLET` internal accounts. @@ -17780,119 +17956,6 @@ components: format: date-time description: Timestamp when the internal account was last updated example: '2025-10-03T12:30:00Z' - InternalAccountListResponse: - type: object - required: - - data - - hasMore - properties: - data: - type: array - description: List of internal accounts matching the filter criteria - items: - $ref: '#/components/schemas/InternalAccount' - hasMore: - type: boolean - description: Indicates if more results are available beyond this page - nextCursor: - type: string - description: Cursor to retrieve the next page of results (only present if hasMore is true) - totalCount: - type: integer - description: Total number of customers matching the criteria (excluding pagination) - SweepRuleDestinationRequest: - title: Sweep Rule Destination Request - type: object - description: The account a rule-based account forwards its incoming payments to. - additionalProperties: false - required: - - accountId - properties: - accountId: - type: string - description: Reference to the account that receives the forwarded funds. May be an external account or another internal account, but never a `RULE_BASED` internal account — that account's own rule would forward the funds on again. The destination may be denominated in a different currency, in which case the forward is converted at the prevailing rate. - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - paymentRail: - description: The payment rail to use when forwarding to an external account. Must be one of the rails supported by the destination account. If omitted, a rail is selected automatically for each forward. Not accepted when the destination is an internal account, which settles without a payment rail. - allOf: - - $ref: '#/components/schemas/PaymentRail' - PurposeOfPayment: - type: string - description: The purpose of the payment. This may be required when sending to certain geographies (e.g. India). - enum: - - GIFT - - SELF - - GOODS_OR_SERVICES - - EDUCATION - - HEALTH_OR_MEDICAL - - REAL_ESTATE_PURCHASE - - TAX_PAYMENT - - LOAN_PAYMENT - - UTILITY_BILL - - DONATION - - TRAVEL - - FAMILY_SUPPORT - - SALARY_PAYMENT - - OTHER - SweepRuleRequest: - title: Sweep Rule Request - type: object - description: The routing rule attached to a rule-based account. Every payment that settles into the account is forwarded to the rule's destination, carrying the payment metadata configured here. - additionalProperties: false - required: - - destination - properties: - destination: - allOf: - - $ref: '#/components/schemas/SweepRuleDestinationRequest' - description: Where funds that settle into this account are forwarded. - purposeOfPayment: - allOf: - - $ref: '#/components/schemas/PurposeOfPayment' - description: The purpose of payment applied to each forward. Required by some destination geographies. - example: SELF - description: - type: string - maxLength: 255 - description: Free-form description recorded on each forward. Not delivered to the recipient; use `remittanceInformation` for that. - example: Rent forwarding - remittanceInformation: - type: string - maxLength: 1024 - description: 'Free-form information that travels with each forward 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 - InternalAccountCreateRequest: - title: Internal Account Create Request - type: object - description: Request body for `POST /customers/internal-accounts`. Only `RULE_BASED` accounts can be created directly, and they always carry a `sweepRule`. - additionalProperties: false - required: - - customerId - - type - - currency - properties: - customerId: - type: string - description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a forward cannot be completed. - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: - allOf: - - $ref: '#/components/schemas/InternalAccountType' - 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: '#/components/schemas/SweepRuleRequest' - description: The routing rule for the account. Required when `type` is `RULE_BASED`. Error403: type: object required: @@ -17928,6 +17991,26 @@ components: type: object description: Additional error details additionalProperties: true + InternalAccountListResponse: + type: object + required: + - data + - hasMore + properties: + data: + type: array + description: List of internal accounts matching the filter criteria + items: + $ref: '#/components/schemas/InternalAccount' + hasMore: + type: boolean + description: Indicates if more results are available beyond this page + nextCursor: + type: string + description: Cursor to retrieve the next page of results (only present if hasMore is true) + totalCount: + type: integer + description: Total number of customers matching the criteria (excluding pagination) PlatformInternalAccountListResponse: type: object required: @@ -24143,27 +24226,6 @@ components: - SENDING - RECEIVING description: The side of the quote which should be locked and specified in the `lockedCurrencyAmount`. For example, if I want to send exactly $5 MXN from my wallet, I would set this to "sending", and the `lockedCurrencyAmount` to 500 (in cents). If I want the receiver to receive exactly $10 USD, I would set this to "receiving" and the `lockedCurrencyAmount` to 10000 (in cents). - PlatformFeeOverride: - type: object - description: Overrides the platform-collected fee for this transaction. When present, it replaces any configured platform-collected fees that would otherwise apply to the transaction. Currently only supported when the quote's source currency is USD; the fixed fee must be denominated in the source currency. - required: - - platformFixedFee - - platformVariableFeeBps - properties: - platformFixedFee: - $ref: '#/components/schemas/FixedFee' - description: Fixed fee charged for this transaction. Must be denominated in the quote's source currency (USD today). - platformVariableFeeBps: - type: integer - minimum: 0 - maximum: 10000 - description: Variable fee in basis points (1 bps = 0.01%) to apply to the transaction's source-currency amount. - example: 30 - example: - platformFixedFee: - amount: 50 - currency: USD - platformVariableFeeBps: 30 QuoteRequest: type: object required: diff --git a/openapi/components/schemas/customers/InternalAccount.yaml b/openapi/components/schemas/customers/InternalAccount.yaml index ef8709480..fcb541d42 100644 --- a/openapi/components/schemas/customers/InternalAccount.yaml +++ b/openapi/components/schemas/customers/InternalAccount.yaml @@ -32,6 +32,19 @@ properties: description: Payment instructions for funding the account items: $ref: ../common/PaymentInstructions.yaml + label: + type: string + maxLength: 255 + description: >- + The platform-supplied label recorded when the account was created. Null + for accounts that carry none. + example: invoice-4417 + sweepRule: + allOf: + - $ref: SweepRule.yaml + description: >- + The routing rule attached to this account. Null for accounts that carry + no rule, which is every account other than a `RULE_BASED` one. privateEnabled: type: boolean description: >- diff --git a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml index da6cc5450..752a3f18a 100644 --- a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml +++ b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml @@ -1,7 +1,7 @@ title: Internal Account Create Request type: object description: >- - Request body for `POST /customers/internal-accounts`. Only `RULE_BASED` + Request body for `POST /internal-accounts`. Only `RULE_BASED` accounts can be created directly, and they always carry a `sweepRule`. additionalProperties: false required: diff --git a/openapi/components/schemas/customers/InternalAccountType.yaml b/openapi/components/schemas/customers/InternalAccountType.yaml index a9f3a7faf..9a0573ce4 100644 --- a/openapi/components/schemas/customers/InternalAccountType.yaml +++ b/openapi/components/schemas/customers/InternalAccountType.yaml @@ -22,4 +22,4 @@ description: >- - `RULE_BASED`: 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. Created with - `POST /customers/internal-accounts`. + `POST /internal-accounts`. diff --git a/openapi/components/schemas/customers/SweepRule.yaml b/openapi/components/schemas/customers/SweepRule.yaml new file mode 100644 index 000000000..cefb43fdf --- /dev/null +++ b/openapi/components/schemas/customers/SweepRule.yaml @@ -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 forwarded. + 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 forwarded. Zero means no floor applies, which is the case for a + 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 forwarded. + purposeOfPayment: + allOf: + - $ref: ../quotes/PurposeOfPayment.yaml + description: The purpose of payment applied to each forward. + example: SELF + description: + type: string + description: >- + Free-form description recorded on each forward. Not delivered to the + recipient. + example: Rent forwarding + remittanceInformation: + type: string + description: >- + Free-form information that travels with each forward to the recipient. + example: Unit 4B March + platformFeeOverride: + allOf: + - $ref: ../quotes/PlatformFeeOverride.yaml + description: >- + Fee terms applied to every forward this rule drives. Null when the + platform's configured fees apply. diff --git a/openapi/components/schemas/customers/SweepRuleDestination.yaml b/openapi/components/schemas/customers/SweepRuleDestination.yaml new file mode 100644 index 000000000..8adfabb97 --- /dev/null +++ b/openapi/components/schemas/customers/SweepRuleDestination.yaml @@ -0,0 +1,18 @@ +title: Sweep Rule Destination +type: object +description: >- + Where a rule-based account's credits are forwarded. +required: + - accountId +properties: + accountId: + type: string + description: The account that receives the forwarded funds. + example: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + paymentRail: + allOf: + - $ref: ../common/PaymentRail.yaml + description: >- + The rail each forward is sent over. Null when a rail is selected + automatically per forward, in which case none is resolved ahead of time. + example: ACH diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 81604d560..de3242f66 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -187,6 +187,8 @@ paths: $ref: paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml /customers/external-accounts/{externalAccountId}/untrust/confirm: $ref: paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml + /internal-accounts: + $ref: paths/internal_accounts.yaml /customers/internal-accounts: $ref: paths/customers/customers_internal_accounts.yaml /platform/internal-accounts: diff --git a/openapi/paths/customers/customers_internal_accounts.yaml b/openapi/paths/customers/customers_internal_accounts.yaml index 1be79f610..8ee266450 100644 --- a/openapi/paths/customers/customers_internal_accounts.yaml +++ b/openapi/paths/customers/customers_internal_accounts.yaml @@ -1,137 +1,3 @@ -post: - summary: Create a Customer internal account - description: | - Create a rule-based internal account for an existing customer. - - A rule-based account is an additional account number for a customer that - already holds an account in the same currency. It carries a routing rule: - every payment that settles into it is attributed to that account number and - then forwarded to the rule's destination, so you can issue one number per - payer and reconcile incoming payments without matching on remittance text. - - Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, - `INTERNAL_CRYPTO`, and `EMBEDDED_WALLET` accounts are provisioned - automatically when a customer is created or approved. - - The response carries the account's own `fundingPaymentInstructions` — the - routable number payers send to. Rule-based accounts are currently available - in `USD` for individual customers, and must be enabled for your platform - before this endpoint accepts requests. - - Creating an account mints a new account number that cannot be reversed, so - an `Idempotency-Key` header is required. A retry carrying the same key - returns the account created by the first request with a `200` instead of a - `201`; reusing a key for a materially different account is rejected with - `409`. - operationId: createCustomerInternalAccount - tags: - - Internal Accounts - security: - - BasicAuth: [] - parameters: - - name: Idempotency-Key - in: header - description: > - A unique identifier for the request. Required, because creating the - account mints an account number that cannot be reversed. Retries must - carry the same key. - required: true - schema: - type: string - example: 550e8400-e29b-41d4-a716-446655440000 - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/customers/InternalAccountCreateRequest.yaml - examples: - externalDestination: - summary: Forward every payment to an external account over ACH - value: - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: RULE_BASED - currency: USD - label: invoice-4417 - sweepRule: - destination: - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - paymentRail: ACH - purposeOfPayment: SELF - description: Rent forwarding - remittanceInformation: Unit 4B March - internalDestination: - summary: Forward every payment to another internal account - value: - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: RULE_BASED - currency: USD - label: payer-northwind - sweepRule: - destination: - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - responses: - '200': - description: >- - The `Idempotency-Key` was already used to create this account. The - account created by the first request is returned unchanged. - content: - application/json: - schema: - $ref: ../../components/schemas/customers/InternalAccount.yaml - '201': - description: >- - Internal account created. `fundingPaymentInstructions` carries the - account's own number. - content: - application/json: - schema: - $ref: ../../components/schemas/customers/InternalAccount.yaml - '400': - description: >- - Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` - is missing, when the currency is not supported for rule-based accounts, - when the customer has no account in that currency yet, when the - destination cannot accept the requested payment rail, and for general - invalid parameters. - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error400.yaml - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error401.yaml - '403': - description: >- - Forbidden - rule-based accounts are not enabled for this platform. - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error403.yaml - '404': - description: Customer or destination account not found - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error404.yaml - '409': - description: >- - Conflict - the `Idempotency-Key` was already used to create a different - account, or the account for this key is still being provisioned. Retry - with the same key. - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error409.yaml - '500': - description: Internal service error - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error500.yaml get: summary: List Customer internal accounts description: | diff --git a/openapi/paths/internal_accounts.yaml b/openapi/paths/internal_accounts.yaml new file mode 100644 index 000000000..4c9850576 --- /dev/null +++ b/openapi/paths/internal_accounts.yaml @@ -0,0 +1,134 @@ +post: + summary: Create an internal account + description: | + Create a rule-based internal account for an existing customer. + + A rule-based account is an additional account number for a customer that + already holds an account in the same currency. It carries a routing rule: + every payment that settles into it is attributed to that account number and + then forwarded to the rule's destination, so you can issue one number per + payer and reconcile incoming payments without matching on remittance text. + + Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, + `INTERNAL_CRYPTO`, and `EMBEDDED_WALLET` accounts are provisioned + automatically when a customer is created or approved. + + The response carries the account's own `fundingPaymentInstructions` — the + routable number payers send to. Rule-based accounts are currently available + in `USD` for individual customers, and must be enabled for your platform + before this endpoint accepts requests. + + Creating an account mints a new account number that cannot be reversed, so + an `Idempotency-Key` header is required. A retry carrying the same key + returns the account created by the first request with a `200` instead of a + `201`; reusing a key for a materially different account is rejected with + `409`. + operationId: createInternalAccount + tags: + - Internal Accounts + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + description: > + A unique identifier for the request. Required, because creating the + account mints an account number that cannot be reversed. Retries must + carry the same key. + required: true + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/customers/InternalAccountCreateRequest.yaml + examples: + externalDestination: + summary: Forward every payment to an external account over ACH + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: RULE_BASED + currency: USD + label: invoice-4417 + sweepRule: + destination: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: ACH + purposeOfPayment: SELF + description: Rent forwarding + remittanceInformation: Unit 4B March + internalDestination: + summary: Forward every payment to another internal account + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: RULE_BASED + currency: USD + label: payer-northwind + sweepRule: + destination: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + responses: + '200': + description: >- + The `Idempotency-Key` was already used to create this account. The + account created by the first request is returned unchanged. + content: + application/json: + schema: + $ref: ../components/schemas/customers/InternalAccount.yaml + '201': + description: >- + Internal account created. `fundingPaymentInstructions` carries the + account's own number. + content: + application/json: + schema: + $ref: ../components/schemas/customers/InternalAccount.yaml + '400': + description: >- + Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` + is missing, when the currency is not supported for rule-based accounts, + when the customer has no account in that currency yet, when the + destination cannot accept the requested payment rail, and for general + invalid parameters. + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error401.yaml + '403': + description: >- + Forbidden - rule-based accounts are not enabled for this platform. + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error403.yaml + '404': + description: Customer or destination account not found + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error404.yaml + '409': + description: >- + Conflict - the `Idempotency-Key` was already used to create a different + account, or the account for this key is still being provisioned. Retry + with the same key. + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error500.yaml From 7f85174de0593c9c656e3bc647f9afa92427f41f Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 2 Sep 2026 18:29:45 +0000 Subject: [PATCH 03/16] fix: require sweepRule on the create request 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. --- mintlify/openapi.yaml | 1 + openapi.yaml | 1 + .../schemas/customers/InternalAccountCreateRequest.yaml | 1 + 3 files changed, 3 insertions(+) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 3179d979d..7e99211bf 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -15052,6 +15052,7 @@ components: - customerId - type - currency + - sweepRule properties: customerId: type: string diff --git a/openapi.yaml b/openapi.yaml index 3179d979d..7e99211bf 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -15052,6 +15052,7 @@ components: - customerId - type - currency + - sweepRule properties: customerId: type: string diff --git a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml index 752a3f18a..25fb3922a 100644 --- a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml +++ b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml @@ -8,6 +8,7 @@ required: - customerId - type - currency + - sweepRule properties: customerId: type: string From ed7c1154b36a69bf06f4bda347329a9cbdd248f9 Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 2 Sep 2026 20:26:01 +0000 Subject: [PATCH 04/16] docs: say sweep, and drop the held-in-account outcome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 107 ++++++++---------- openapi.yaml | 107 ++++++++---------- .../InternalAccountCreateRequest.yaml | 2 +- .../customers/InternalAccountType.yaml | 2 +- .../schemas/customers/SweepRule.yaml | 16 +-- .../customers/SweepRuleDestination.yaml | 8 +- .../SweepRuleDestinationRequest.yaml | 12 +- .../schemas/customers/SweepRuleRequest.yaml | 12 +- .../schemas/webhooks/SweepFailure.yaml | 29 ++--- .../schemas/webhooks/SweepFailureOutcome.yaml | 11 -- .../schemas/webhooks/SweepFailureReason.yaml | 8 +- openapi/paths/internal_accounts.yaml | 10 +- openapi/webhooks/sweep.yaml | 18 ++- 13 files changed, 150 insertions(+), 192 deletions(-) delete mode 100644 openapi/components/schemas/webhooks/SweepFailureOutcome.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 7e99211bf..dcd4aaa59 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2100,7 +2100,7 @@ paths: A rule-based account is an additional account number for a customer that already holds an account in the same currency. It carries a routing rule: every payment that settles into it is attributed to that account number and - then forwarded to the rule's destination, so you can issue one number per + then swept to the rule's destination, so you can issue one number per payer and reconcile incoming payments without matching on remittance text. Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, @@ -2112,6 +2112,12 @@ paths: in `USD` for individual customers, and must be enabled for your platform before this endpoint accepts requests. + Sandbox platforms can create rule-based accounts and exercise the whole + flow. The account number is generated locally rather than issued by a + partner bank, and `POST /sandbox/internal-accounts/{accountId}/fund` stands + in for a settled deposit — funding the account triggers its sweep just as a + real deposit would. + Creating an account mints a new account number that cannot be reversed, so an `Idempotency-Key` header is required. A retry carrying the same key returns the account created by the first request with a `200` instead of a @@ -2150,7 +2156,7 @@ paths: accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 paymentRail: ACH purposeOfPayment: SELF - description: Rent forwarding + description: Rent sweep remittanceInformation: Unit 4B March internalDestination: summary: Forward every payment to another internal account @@ -12014,7 +12020,7 @@ webhooks: post: summary: Sweep webhook description: | - Webhook that is called when a payment settles into a rule-based internal account but cannot be forwarded to the destination configured on the account's rule. + Webhook that is called when a payment settles into a rule-based internal account but cannot be swept to the destination configured on the account's rule. This endpoint should be implemented by clients of the Grid API. @@ -12031,7 +12037,7 @@ webhooks: ### Event types - - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `outcome` says where the funds ended up: moved to the customer's account in the same currency, or still held in the rule-based account. A failed forward that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. + - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the customer's account in the same currency the funds were moved to instead: a rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `incomingTransactionId`. operationId: sweepWebhook @@ -12065,10 +12071,9 @@ webhooks: incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 sendTransactionId: null reason: BELOW_MINIMUM - outcome: MOVED_TO_CANONICAL_ACCOUNT canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - heldInRuleBasedAccount: - summary: The forward was sent but did not go through, and the funds stayed put + executionFailed: + summary: The sweep was sent but did not go through, so the funds were moved to the customer's account value: id: Webhook:019542f5-b3e7-1d02-0000-000000000051 type: SWEEP.FAILED @@ -12086,8 +12091,7 @@ webhooks: incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000012 sendTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 reason: EXECUTION_FAILED - outcome: HELD_IN_RULE_BASED_ACCOUNT - canonicalAccountId: null + canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 responses: '200': description: | @@ -14981,21 +14985,21 @@ components: - `INTERNAL_FIAT`: A Grid-managed fiat holding account (for example, the USD holding account used as the source for Payouts flows). - `INTERNAL_CRYPTO`: A Grid-managed crypto holding account denominated in a stablecoin such as USDC. - `EMBEDDED_WALLET`: A self-custodial Embedded Wallet provisioned for the customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. - - `RULE_BASED`: 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. Created with `POST /internal-accounts`. + - `RULE_BASED`: An additional account number for an existing customer, with a routing rule attached, so incoming payments can be attributed to a specific payer and swept automatically. Created with `POST /internal-accounts`. SweepRuleDestinationRequest: title: Sweep Rule Destination Request type: object - description: The account a rule-based account forwards its incoming payments to. + 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 forwarded funds. May be an external account or another internal account, but never a `RULE_BASED` internal account — that account's own rule would forward the funds on again. The destination may be denominated in a different currency, in which case the forward is converted at the prevailing rate. + 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 forwarding to an external account. Must be one of the rails supported by the destination account. If omitted, a rail is selected automatically for each forward. Not accepted when the destination is an internal account, which settles without a payment rail. + 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: '#/components/schemas/PaymentRail' PurposeOfPayment: @@ -15019,7 +15023,7 @@ components: SweepRuleRequest: title: Sweep Rule Request type: object - description: The routing rule attached to a rule-based account. Every payment that settles into the account is forwarded to the rule's destination, carrying the payment metadata configured here. + 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 @@ -15027,21 +15031,21 @@ components: destination: allOf: - $ref: '#/components/schemas/SweepRuleDestinationRequest' - description: Where funds that settle into this account are forwarded. + description: Where funds that settle into this account are swept. purposeOfPayment: allOf: - $ref: '#/components/schemas/PurposeOfPayment' - description: The purpose of payment applied to each forward. Required by some destination geographies. + 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 forward. Not delivered to the recipient; use `remittanceInformation` for that. - example: Rent forwarding + 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 forward 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.' + 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 InternalAccountCreateRequest: title: Internal Account Create Request @@ -15056,7 +15060,7 @@ components: properties: customerId: type: string - description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a forward cannot be completed. + description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a sweep cannot be completed. example: Customer:019542f5-b3e7-1d02-0000-000000000001 type: allOf: @@ -17829,18 +17833,18 @@ components: SweepRuleDestination: title: Sweep Rule Destination type: object - description: Where a rule-based account's credits are forwarded. + description: Where a rule-based account's credits are swept. required: - accountId properties: accountId: type: string - description: The account that receives the forwarded funds. + description: The account that receives the swept funds. example: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 paymentRail: allOf: - $ref: '#/components/schemas/PaymentRail' - description: The rail each forward is sent over. Null when a rail is selected automatically per forward, in which case none is resolved ahead of time. + 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 PlatformFeeOverride: type: object @@ -17873,32 +17877,32 @@ components: destination: allOf: - $ref: '#/components/schemas/SweepRuleDestination' - description: Where funds that settle into this account are forwarded. + description: Where funds that settle into this account are swept. minimumAmount: allOf: - $ref: '#/components/schemas/CurrencyAmount' - 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 forwarded. Zero means no floor applies, which is the case for a 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.' + 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 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: '#/components/schemas/CurrencyAmount' - description: The largest balance the corridor to the destination can carry. Null means no ceiling applies. A settled balance above it is not forwarded. + 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: - $ref: '#/components/schemas/PurposeOfPayment' - description: The purpose of payment applied to each forward. + description: The purpose of payment applied to each sweep. example: SELF description: type: string - description: Free-form description recorded on each forward. Not delivered to the recipient. - example: Rent forwarding + 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 forward to the recipient. + description: Free-form information that travels with each sweep to the recipient. example: Unit 4B March platformFeeOverride: allOf: - $ref: '#/components/schemas/PlatformFeeOverride' - description: Fee terms applied to every forward this rule drives. Null when the platform's configured fees apply. + description: Fee terms applied to every sweep this rule drives. Null when the platform's configured fees apply. InternalAccount: type: object required: @@ -27520,38 +27524,26 @@ components: - QUOTE_FAILED - EXECUTION_FAILED description: | - Why the payment could not be forwarded to the rule's destination. + 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 forward. | - | `QUOTE_FAILED` | Grid could not price the forward to the destination. | - | `EXECUTION_FAILED` | The forward was priced but the resulting payment did not go through. | - SweepFailureOutcome: - type: string - enum: - - MOVED_TO_CANONICAL_ACCOUNT - - HELD_IN_RULE_BASED_ACCOUNT - description: | - Where the funds ended up after the forward failed. - - | Outcome | Description | - |---------|-------------| - | `MOVED_TO_CANONICAL_ACCOUNT` | The funds were moved to the customer's account in the same currency, identified by `canonicalAccountId`. | - | `HELD_IN_RULE_BASED_ACCOUNT` | The funds remain in the rule-based account. Grid retries the move when the next payment settles into the account. | + | `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. | SweepFailure: title: Sweep Failure type: object - description: A payment that settled into a rule-based account but did not reach the destination of the account's rule. + description: A payment that settled into a rule-based account but was not swept to the destination of the account's rule. required: - sourceAccountId - destinationAccountId - amount - incomingTransactionId - reason - - outcome + - canonicalAccountId properties: sourceAccountId: type: string @@ -27564,7 +27556,7 @@ components: amount: allOf: - $ref: '#/components/schemas/CurrencyAmount' - description: The amount that could not be forwarded. + description: The amount that could not be swept. incomingTransactionId: type: string description: The transaction that brought the funds into the rule-based account. Delivery of this event is at-least-once, so deduplicate on this value. @@ -27573,23 +27565,16 @@ components: type: - string - 'null' - description: The outgoing transaction created for the forward, when the forward got far enough to create one. Null when the forward was never attempted. Use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same forward rather than counting the failure twice. + description: The operation covering both the deposit into the rule-based account and the sweep out of it, present once the sweep got far enough to create one. Null when the sweep was never attempted. Use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting the failure twice. example: Transaction:019542f5-b3e7-1d02-0000-000000000011 reason: allOf: - $ref: '#/components/schemas/SweepFailureReason' - description: Why the forward could not be completed. + description: Why the sweep could not be completed. example: BELOW_MINIMUM - outcome: - allOf: - - $ref: '#/components/schemas/SweepFailureOutcome' - description: Where the funds ended up. - example: MOVED_TO_CANONICAL_ACCOUNT canonicalAccountId: - type: - - string - - 'null' - description: The customer's account in the same currency that the funds were moved to. Null when `outcome` is `HELD_IN_RULE_BASED_ACCOUNT`. + type: string + description: The customer's account in the same currency that the funds were moved to. A rule-based account never holds a balance, so a sweep that cannot reach its destination always lands the funds here. example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 SweepWebhook: allOf: diff --git a/openapi.yaml b/openapi.yaml index 7e99211bf..dcd4aaa59 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2100,7 +2100,7 @@ paths: A rule-based account is an additional account number for a customer that already holds an account in the same currency. It carries a routing rule: every payment that settles into it is attributed to that account number and - then forwarded to the rule's destination, so you can issue one number per + then swept to the rule's destination, so you can issue one number per payer and reconcile incoming payments without matching on remittance text. Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, @@ -2112,6 +2112,12 @@ paths: in `USD` for individual customers, and must be enabled for your platform before this endpoint accepts requests. + Sandbox platforms can create rule-based accounts and exercise the whole + flow. The account number is generated locally rather than issued by a + partner bank, and `POST /sandbox/internal-accounts/{accountId}/fund` stands + in for a settled deposit — funding the account triggers its sweep just as a + real deposit would. + Creating an account mints a new account number that cannot be reversed, so an `Idempotency-Key` header is required. A retry carrying the same key returns the account created by the first request with a `200` instead of a @@ -2150,7 +2156,7 @@ paths: accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 paymentRail: ACH purposeOfPayment: SELF - description: Rent forwarding + description: Rent sweep remittanceInformation: Unit 4B March internalDestination: summary: Forward every payment to another internal account @@ -12014,7 +12020,7 @@ webhooks: post: summary: Sweep webhook description: | - Webhook that is called when a payment settles into a rule-based internal account but cannot be forwarded to the destination configured on the account's rule. + Webhook that is called when a payment settles into a rule-based internal account but cannot be swept to the destination configured on the account's rule. This endpoint should be implemented by clients of the Grid API. @@ -12031,7 +12037,7 @@ webhooks: ### Event types - - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `outcome` says where the funds ended up: moved to the customer's account in the same currency, or still held in the rule-based account. A failed forward that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. + - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the customer's account in the same currency the funds were moved to instead: a rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `incomingTransactionId`. operationId: sweepWebhook @@ -12065,10 +12071,9 @@ webhooks: incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 sendTransactionId: null reason: BELOW_MINIMUM - outcome: MOVED_TO_CANONICAL_ACCOUNT canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - heldInRuleBasedAccount: - summary: The forward was sent but did not go through, and the funds stayed put + executionFailed: + summary: The sweep was sent but did not go through, so the funds were moved to the customer's account value: id: Webhook:019542f5-b3e7-1d02-0000-000000000051 type: SWEEP.FAILED @@ -12086,8 +12091,7 @@ webhooks: incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000012 sendTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 reason: EXECUTION_FAILED - outcome: HELD_IN_RULE_BASED_ACCOUNT - canonicalAccountId: null + canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 responses: '200': description: | @@ -14981,21 +14985,21 @@ components: - `INTERNAL_FIAT`: A Grid-managed fiat holding account (for example, the USD holding account used as the source for Payouts flows). - `INTERNAL_CRYPTO`: A Grid-managed crypto holding account denominated in a stablecoin such as USDC. - `EMBEDDED_WALLET`: A self-custodial Embedded Wallet provisioned for the customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. - - `RULE_BASED`: 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. Created with `POST /internal-accounts`. + - `RULE_BASED`: An additional account number for an existing customer, with a routing rule attached, so incoming payments can be attributed to a specific payer and swept automatically. Created with `POST /internal-accounts`. SweepRuleDestinationRequest: title: Sweep Rule Destination Request type: object - description: The account a rule-based account forwards its incoming payments to. + 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 forwarded funds. May be an external account or another internal account, but never a `RULE_BASED` internal account — that account's own rule would forward the funds on again. The destination may be denominated in a different currency, in which case the forward is converted at the prevailing rate. + 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 forwarding to an external account. Must be one of the rails supported by the destination account. If omitted, a rail is selected automatically for each forward. Not accepted when the destination is an internal account, which settles without a payment rail. + 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: '#/components/schemas/PaymentRail' PurposeOfPayment: @@ -15019,7 +15023,7 @@ components: SweepRuleRequest: title: Sweep Rule Request type: object - description: The routing rule attached to a rule-based account. Every payment that settles into the account is forwarded to the rule's destination, carrying the payment metadata configured here. + 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 @@ -15027,21 +15031,21 @@ components: destination: allOf: - $ref: '#/components/schemas/SweepRuleDestinationRequest' - description: Where funds that settle into this account are forwarded. + description: Where funds that settle into this account are swept. purposeOfPayment: allOf: - $ref: '#/components/schemas/PurposeOfPayment' - description: The purpose of payment applied to each forward. Required by some destination geographies. + 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 forward. Not delivered to the recipient; use `remittanceInformation` for that. - example: Rent forwarding + 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 forward 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.' + 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 InternalAccountCreateRequest: title: Internal Account Create Request @@ -15056,7 +15060,7 @@ components: properties: customerId: type: string - description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a forward cannot be completed. + description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a sweep cannot be completed. example: Customer:019542f5-b3e7-1d02-0000-000000000001 type: allOf: @@ -17829,18 +17833,18 @@ components: SweepRuleDestination: title: Sweep Rule Destination type: object - description: Where a rule-based account's credits are forwarded. + description: Where a rule-based account's credits are swept. required: - accountId properties: accountId: type: string - description: The account that receives the forwarded funds. + description: The account that receives the swept funds. example: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 paymentRail: allOf: - $ref: '#/components/schemas/PaymentRail' - description: The rail each forward is sent over. Null when a rail is selected automatically per forward, in which case none is resolved ahead of time. + 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 PlatformFeeOverride: type: object @@ -17873,32 +17877,32 @@ components: destination: allOf: - $ref: '#/components/schemas/SweepRuleDestination' - description: Where funds that settle into this account are forwarded. + description: Where funds that settle into this account are swept. minimumAmount: allOf: - $ref: '#/components/schemas/CurrencyAmount' - 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 forwarded. Zero means no floor applies, which is the case for a 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.' + 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 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: '#/components/schemas/CurrencyAmount' - description: The largest balance the corridor to the destination can carry. Null means no ceiling applies. A settled balance above it is not forwarded. + 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: - $ref: '#/components/schemas/PurposeOfPayment' - description: The purpose of payment applied to each forward. + description: The purpose of payment applied to each sweep. example: SELF description: type: string - description: Free-form description recorded on each forward. Not delivered to the recipient. - example: Rent forwarding + 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 forward to the recipient. + description: Free-form information that travels with each sweep to the recipient. example: Unit 4B March platformFeeOverride: allOf: - $ref: '#/components/schemas/PlatformFeeOverride' - description: Fee terms applied to every forward this rule drives. Null when the platform's configured fees apply. + description: Fee terms applied to every sweep this rule drives. Null when the platform's configured fees apply. InternalAccount: type: object required: @@ -27520,38 +27524,26 @@ components: - QUOTE_FAILED - EXECUTION_FAILED description: | - Why the payment could not be forwarded to the rule's destination. + 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 forward. | - | `QUOTE_FAILED` | Grid could not price the forward to the destination. | - | `EXECUTION_FAILED` | The forward was priced but the resulting payment did not go through. | - SweepFailureOutcome: - type: string - enum: - - MOVED_TO_CANONICAL_ACCOUNT - - HELD_IN_RULE_BASED_ACCOUNT - description: | - Where the funds ended up after the forward failed. - - | Outcome | Description | - |---------|-------------| - | `MOVED_TO_CANONICAL_ACCOUNT` | The funds were moved to the customer's account in the same currency, identified by `canonicalAccountId`. | - | `HELD_IN_RULE_BASED_ACCOUNT` | The funds remain in the rule-based account. Grid retries the move when the next payment settles into the account. | + | `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. | SweepFailure: title: Sweep Failure type: object - description: A payment that settled into a rule-based account but did not reach the destination of the account's rule. + description: A payment that settled into a rule-based account but was not swept to the destination of the account's rule. required: - sourceAccountId - destinationAccountId - amount - incomingTransactionId - reason - - outcome + - canonicalAccountId properties: sourceAccountId: type: string @@ -27564,7 +27556,7 @@ components: amount: allOf: - $ref: '#/components/schemas/CurrencyAmount' - description: The amount that could not be forwarded. + description: The amount that could not be swept. incomingTransactionId: type: string description: The transaction that brought the funds into the rule-based account. Delivery of this event is at-least-once, so deduplicate on this value. @@ -27573,23 +27565,16 @@ components: type: - string - 'null' - description: The outgoing transaction created for the forward, when the forward got far enough to create one. Null when the forward was never attempted. Use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same forward rather than counting the failure twice. + description: The operation covering both the deposit into the rule-based account and the sweep out of it, present once the sweep got far enough to create one. Null when the sweep was never attempted. Use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting the failure twice. example: Transaction:019542f5-b3e7-1d02-0000-000000000011 reason: allOf: - $ref: '#/components/schemas/SweepFailureReason' - description: Why the forward could not be completed. + description: Why the sweep could not be completed. example: BELOW_MINIMUM - outcome: - allOf: - - $ref: '#/components/schemas/SweepFailureOutcome' - description: Where the funds ended up. - example: MOVED_TO_CANONICAL_ACCOUNT canonicalAccountId: - type: - - string - - 'null' - description: The customer's account in the same currency that the funds were moved to. Null when `outcome` is `HELD_IN_RULE_BASED_ACCOUNT`. + type: string + description: The customer's account in the same currency that the funds were moved to. A rule-based account never holds a balance, so a sweep that cannot reach its destination always lands the funds here. example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 SweepWebhook: allOf: diff --git a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml index 25fb3922a..2ff88c1dd 100644 --- a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml +++ b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml @@ -15,7 +15,7 @@ properties: description: >- The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a - forward cannot be completed. + sweep cannot be completed. example: Customer:019542f5-b3e7-1d02-0000-000000000001 type: allOf: diff --git a/openapi/components/schemas/customers/InternalAccountType.yaml b/openapi/components/schemas/customers/InternalAccountType.yaml index 9a0573ce4..ad2c47027 100644 --- a/openapi/components/schemas/customers/InternalAccountType.yaml +++ b/openapi/components/schemas/customers/InternalAccountType.yaml @@ -21,5 +21,5 @@ description: >- - `RULE_BASED`: 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. Created with + payer and swept automatically. Created with `POST /internal-accounts`. diff --git a/openapi/components/schemas/customers/SweepRule.yaml b/openapi/components/schemas/customers/SweepRule.yaml index cefb43fdf..ebb9f8866 100644 --- a/openapi/components/schemas/customers/SweepRule.yaml +++ b/openapi/components/schemas/customers/SweepRule.yaml @@ -10,14 +10,14 @@ properties: destination: allOf: - $ref: SweepRuleDestination.yaml - description: Where funds that settle into this account are forwarded. + 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 forwarded. Zero means no floor applies, which is the case for a + not swept. Zero means no floor applies, which is the case for a 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 @@ -27,26 +27,26 @@ properties: - $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 forwarded. + no ceiling applies. A settled balance above it is not swept. purposeOfPayment: allOf: - $ref: ../quotes/PurposeOfPayment.yaml - description: The purpose of payment applied to each forward. + description: The purpose of payment applied to each sweep. example: SELF description: type: string description: >- - Free-form description recorded on each forward. Not delivered to the + Free-form description recorded on each sweep. Not delivered to the recipient. - example: Rent forwarding + example: Rent sweep remittanceInformation: type: string description: >- - Free-form information that travels with each forward to the recipient. + 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 forward this rule drives. Null when the + Fee terms applied to every sweep this rule drives. Null when the platform's configured fees apply. diff --git a/openapi/components/schemas/customers/SweepRuleDestination.yaml b/openapi/components/schemas/customers/SweepRuleDestination.yaml index 8adfabb97..72e5f9a3d 100644 --- a/openapi/components/schemas/customers/SweepRuleDestination.yaml +++ b/openapi/components/schemas/customers/SweepRuleDestination.yaml @@ -1,18 +1,18 @@ title: Sweep Rule Destination type: object description: >- - Where a rule-based account's credits are forwarded. + Where a rule-based account's credits are swept. required: - accountId properties: accountId: type: string - description: The account that receives the forwarded funds. + 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 forward is sent over. Null when a rail is selected - automatically per forward, in which case none is resolved ahead of time. + 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 diff --git a/openapi/components/schemas/customers/SweepRuleDestinationRequest.yaml b/openapi/components/schemas/customers/SweepRuleDestinationRequest.yaml index f3ee871b6..5f8cd6232 100644 --- a/openapi/components/schemas/customers/SweepRuleDestinationRequest.yaml +++ b/openapi/components/schemas/customers/SweepRuleDestinationRequest.yaml @@ -1,7 +1,7 @@ title: Sweep Rule Destination Request type: object description: >- - The account a rule-based account forwards its incoming payments to. + The account a rule-based account sweeps its incoming payments to. additionalProperties: false required: - accountId @@ -9,17 +9,17 @@ properties: accountId: type: string description: >- - Reference to the account that receives the forwarded funds. May be an + 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 forward the funds on + 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 forward is converted at the prevailing rate. + 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 forwarding to an external account. Must be + 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 forward. Not accepted when the + 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 diff --git a/openapi/components/schemas/customers/SweepRuleRequest.yaml b/openapi/components/schemas/customers/SweepRuleRequest.yaml index 081ec1818..7f8056dc8 100644 --- a/openapi/components/schemas/customers/SweepRuleRequest.yaml +++ b/openapi/components/schemas/customers/SweepRuleRequest.yaml @@ -2,7 +2,7 @@ title: Sweep Rule Request type: object description: >- The routing rule attached to a rule-based account. Every payment that settles - into the account is forwarded to the rule's destination, carrying the payment + into the account is swept to the rule's destination, carrying the payment metadata configured here. additionalProperties: false required: @@ -11,26 +11,26 @@ properties: destination: allOf: - $ref: ./SweepRuleDestinationRequest.yaml - description: Where funds that settle into this account are forwarded. + description: Where funds that settle into this account are swept. purposeOfPayment: allOf: - $ref: ../quotes/PurposeOfPayment.yaml description: >- - The purpose of payment applied to each forward. Required by some + 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 forward. Not delivered to the + Free-form description recorded on each sweep. Not delivered to the recipient; use `remittanceInformation` for that. - example: Rent forwarding + example: Rent sweep remittanceInformation: type: string maxLength: 1024 description: >- - Free-form information that travels with each forward to the recipient. + 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 diff --git a/openapi/components/schemas/webhooks/SweepFailure.yaml b/openapi/components/schemas/webhooks/SweepFailure.yaml index d1f024bfe..578b045db 100644 --- a/openapi/components/schemas/webhooks/SweepFailure.yaml +++ b/openapi/components/schemas/webhooks/SweepFailure.yaml @@ -1,7 +1,7 @@ title: Sweep Failure type: object description: >- - A payment that settled into a rule-based account but did not reach the + A payment that settled into a rule-based account but was not swept to the destination of the account's rule. required: - sourceAccountId @@ -9,7 +9,7 @@ required: - amount - incomingTransactionId - reason - - outcome + - canonicalAccountId properties: sourceAccountId: type: string @@ -24,7 +24,7 @@ properties: amount: allOf: - $ref: ../common/CurrencyAmount.yaml - description: The amount that could not be forwarded. + description: The amount that could not be swept. incomingTransactionId: type: string description: >- @@ -36,26 +36,21 @@ properties: - string - 'null' description: >- - The outgoing transaction created for the forward, when the forward got - far enough to create one. Null when the forward was never attempted. Use - it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for - the same forward rather than counting the failure twice. + The operation covering both the deposit into the rule-based account and + the sweep out of it, present once the sweep got far enough to create one. + Null when the sweep was never attempted. Use it to correlate this event + with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than + counting the failure twice. example: Transaction:019542f5-b3e7-1d02-0000-000000000011 reason: allOf: - $ref: ./SweepFailureReason.yaml - description: Why the forward could not be completed. + description: Why the sweep could not be completed. example: BELOW_MINIMUM - outcome: - allOf: - - $ref: ./SweepFailureOutcome.yaml - description: Where the funds ended up. - example: MOVED_TO_CANONICAL_ACCOUNT canonicalAccountId: - type: - - string - - 'null' + type: string description: >- The customer's account in the same currency that the funds were moved to. - Null when `outcome` is `HELD_IN_RULE_BASED_ACCOUNT`. + A rule-based account never holds a balance, so a sweep that cannot reach + its destination always lands the funds here. example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 diff --git a/openapi/components/schemas/webhooks/SweepFailureOutcome.yaml b/openapi/components/schemas/webhooks/SweepFailureOutcome.yaml deleted file mode 100644 index 4064b3cb8..000000000 --- a/openapi/components/schemas/webhooks/SweepFailureOutcome.yaml +++ /dev/null @@ -1,11 +0,0 @@ -type: string -enum: - - MOVED_TO_CANONICAL_ACCOUNT - - HELD_IN_RULE_BASED_ACCOUNT -description: | - Where the funds ended up after the forward failed. - - | Outcome | Description | - |---------|-------------| - | `MOVED_TO_CANONICAL_ACCOUNT` | The funds were moved to the customer's account in the same currency, identified by `canonicalAccountId`. | - | `HELD_IN_RULE_BASED_ACCOUNT` | The funds remain in the rule-based account. Grid retries the move when the next payment settles into the account. | diff --git a/openapi/components/schemas/webhooks/SweepFailureReason.yaml b/openapi/components/schemas/webhooks/SweepFailureReason.yaml index d2c69b368..a7c9974b5 100644 --- a/openapi/components/schemas/webhooks/SweepFailureReason.yaml +++ b/openapi/components/schemas/webhooks/SweepFailureReason.yaml @@ -6,12 +6,12 @@ enum: - QUOTE_FAILED - EXECUTION_FAILED description: | - Why the payment could not be forwarded to the rule's destination. + 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 forward. | - | `QUOTE_FAILED` | Grid could not price the forward to the destination. | - | `EXECUTION_FAILED` | The forward was priced but the resulting payment did not go through. | + | `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. | diff --git a/openapi/paths/internal_accounts.yaml b/openapi/paths/internal_accounts.yaml index 4c9850576..21b7a6ae7 100644 --- a/openapi/paths/internal_accounts.yaml +++ b/openapi/paths/internal_accounts.yaml @@ -6,7 +6,7 @@ post: A rule-based account is an additional account number for a customer that already holds an account in the same currency. It carries a routing rule: every payment that settles into it is attributed to that account number and - then forwarded to the rule's destination, so you can issue one number per + then swept to the rule's destination, so you can issue one number per payer and reconcile incoming payments without matching on remittance text. Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, @@ -18,6 +18,12 @@ post: in `USD` for individual customers, and must be enabled for your platform before this endpoint accepts requests. + Sandbox platforms can create rule-based accounts and exercise the whole + flow. The account number is generated locally rather than issued by a + partner bank, and `POST /sandbox/internal-accounts/{accountId}/fund` stands + in for a settled deposit — funding the account triggers its sweep just as a + real deposit would. + Creating an account mints a new account number that cannot be reversed, so an `Idempotency-Key` header is required. A retry carrying the same key returns the account created by the first request with a `200` instead of a @@ -58,7 +64,7 @@ post: accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 paymentRail: ACH purposeOfPayment: SELF - description: Rent forwarding + description: Rent sweep remittanceInformation: Unit 4B March internalDestination: summary: Forward every payment to another internal account diff --git a/openapi/webhooks/sweep.yaml b/openapi/webhooks/sweep.yaml index e386f445b..d3ec1770f 100644 --- a/openapi/webhooks/sweep.yaml +++ b/openapi/webhooks/sweep.yaml @@ -2,7 +2,7 @@ post: summary: Sweep webhook description: > Webhook that is called when a payment settles into a rule-based internal - account but cannot be forwarded to the destination configured on the + account but cannot be swept to the destination configured on the account's rule. @@ -34,10 +34,10 @@ post: - `SWEEP.FAILED` — Fired when the funds do not reach the rule's - destination. `reason` says why, and `outcome` says where the funds ended up: - moved to the customer's account in the same currency, or still held in the - rule-based account. A failed forward that also created an outgoing payment - fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on + destination. `reason` says why, and `canonicalAccountId` is the customer's + account in the same currency the funds were moved to instead: a rule-based + account never holds a balance. A failed sweep that also created an outgoing + payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. @@ -74,10 +74,9 @@ post: incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 sendTransactionId: null reason: BELOW_MINIMUM - outcome: MOVED_TO_CANONICAL_ACCOUNT canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - heldInRuleBasedAccount: - summary: The forward was sent but did not go through, and the funds stayed put + executionFailed: + summary: The sweep was sent but did not go through, so the funds were moved to the customer's account value: id: Webhook:019542f5-b3e7-1d02-0000-000000000051 type: SWEEP.FAILED @@ -95,8 +94,7 @@ post: incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000012 sendTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 reason: EXECUTION_FAILED - outcome: HELD_IN_RULE_BASED_ACCOUNT - canonicalAccountId: null + canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 responses: '200': description: > From 7426a03e25ac666862ad2596c72f2dfe4d289904 Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 2 Sep 2026 20:35:26 +0000 Subject: [PATCH 05/16] docs: describe rule-based accounts by their holder, not the customer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 31 ++++++++++--------- openapi.yaml | 31 ++++++++++--------- .../InternalAccountCreateRequest.yaml | 3 +- .../customers/InternalAccountType.yaml | 6 ++-- .../schemas/webhooks/SweepFailure.yaml | 7 +++-- openapi/paths/internal_accounts.yaml | 19 ++++++------ openapi/webhooks/sweep.yaml | 11 ++++--- 7 files changed, 57 insertions(+), 51 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index dcd4aaa59..85f56e835 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2095,21 +2095,22 @@ paths: post: summary: Create an internal account description: | - Create a rule-based internal account for an existing customer. + Create a rule-based internal account. - A rule-based account is an additional account number for a customer that - already holds an account in the same currency. It carries a routing rule: - every payment that settles into it is attributed to that account number and - then swept to the rule's destination, so you can issue one number per - payer and reconcile incoming payments without matching on remittance text. + A rule-based account is an additional account number for an account holder + that already holds an account in the same currency. It carries a routing + rule: every payment that settles into it is attributed to that account + number and then swept to the rule's destination, so you can issue one number + per payer and reconcile incoming payments without matching on remittance + text. Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, `INTERNAL_CRYPTO`, and `EMBEDDED_WALLET` accounts are provisioned - automatically when a customer is created or approved. + automatically when an account holder is created or approved. The response carries the account's own `fundingPaymentInstructions` — the routable number payers send to. Rule-based accounts are currently available - in `USD` for individual customers, and must be enabled for your platform + in `USD`, for individual customers, and must be enabled for your platform before this endpoint accepts requests. Sandbox platforms can create rule-based accounts and exercise the whole @@ -2182,7 +2183,7 @@ paths: schema: $ref: '#/components/schemas/InternalAccount' '400': - description: Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` is missing, when the currency is not supported for rule-based accounts, when the customer has no account in that currency yet, when the destination cannot accept the requested payment rail, and for general invalid parameters. + description: Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` is missing, when the currency is not supported for rule-based accounts, when the account holder has no account in that currency yet, when the destination cannot accept the requested payment rail, and for general invalid parameters. content: application/json: schema: @@ -12037,7 +12038,7 @@ webhooks: ### Event types - - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the customer's account in the same currency the funds were moved to instead: a rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. + - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the account holder's own account in the same currency the funds were moved to instead — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `incomingTransactionId`. operationId: sweepWebhook @@ -12053,7 +12054,7 @@ webhooks: $ref: '#/components/schemas/SweepWebhook' examples: movedToCanonicalAccount: - summary: The amount was below what the corridor carries, so it was moved to the customer's account + summary: The amount was below what the corridor carries, so it was moved to the account holder's own account value: id: Webhook:019542f5-b3e7-1d02-0000-000000000050 type: SWEEP.FAILED @@ -12073,7 +12074,7 @@ webhooks: reason: BELOW_MINIMUM canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 executionFailed: - summary: The sweep was sent but did not go through, so the funds were moved to the customer's account + summary: The sweep was sent but did not go through, so the funds were moved to the account holder's own account value: id: Webhook:019542f5-b3e7-1d02-0000-000000000051 type: SWEEP.FAILED @@ -14985,7 +14986,7 @@ components: - `INTERNAL_FIAT`: A Grid-managed fiat holding account (for example, the USD holding account used as the source for Payouts flows). - `INTERNAL_CRYPTO`: A Grid-managed crypto holding account denominated in a stablecoin such as USDC. - `EMBEDDED_WALLET`: A self-custodial Embedded Wallet provisioned for the customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. - - `RULE_BASED`: An additional account number for an existing customer, with a routing rule attached, so incoming payments can be attributed to a specific payer and swept automatically. Created with `POST /internal-accounts`. + - `RULE_BASED`: An additional account number for an existing account holder, with a routing rule attached, so incoming payments can be attributed to a specific payer and swept automatically. Created with `POST /internal-accounts`. SweepRuleDestinationRequest: title: Sweep Rule Destination Request type: object @@ -15060,7 +15061,7 @@ components: properties: customerId: type: string - description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a sweep cannot be completed. + description: 'The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a sweep cannot be completed. Required today: platform-owned rule-based accounts are not yet available.' example: Customer:019542f5-b3e7-1d02-0000-000000000001 type: allOf: @@ -27574,7 +27575,7 @@ components: example: BELOW_MINIMUM canonicalAccountId: type: string - description: The customer's account in the same currency that the funds were moved to. A rule-based account never holds a balance, so a sweep that cannot reach its destination always lands the funds here. + description: The account holder's own account in the same currency that the funds were moved to — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance, so a sweep that cannot reach its destination always lands the funds here. example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 SweepWebhook: allOf: diff --git a/openapi.yaml b/openapi.yaml index dcd4aaa59..85f56e835 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2095,21 +2095,22 @@ paths: post: summary: Create an internal account description: | - Create a rule-based internal account for an existing customer. + Create a rule-based internal account. - A rule-based account is an additional account number for a customer that - already holds an account in the same currency. It carries a routing rule: - every payment that settles into it is attributed to that account number and - then swept to the rule's destination, so you can issue one number per - payer and reconcile incoming payments without matching on remittance text. + A rule-based account is an additional account number for an account holder + that already holds an account in the same currency. It carries a routing + rule: every payment that settles into it is attributed to that account + number and then swept to the rule's destination, so you can issue one number + per payer and reconcile incoming payments without matching on remittance + text. Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, `INTERNAL_CRYPTO`, and `EMBEDDED_WALLET` accounts are provisioned - automatically when a customer is created or approved. + automatically when an account holder is created or approved. The response carries the account's own `fundingPaymentInstructions` — the routable number payers send to. Rule-based accounts are currently available - in `USD` for individual customers, and must be enabled for your platform + in `USD`, for individual customers, and must be enabled for your platform before this endpoint accepts requests. Sandbox platforms can create rule-based accounts and exercise the whole @@ -2182,7 +2183,7 @@ paths: schema: $ref: '#/components/schemas/InternalAccount' '400': - description: Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` is missing, when the currency is not supported for rule-based accounts, when the customer has no account in that currency yet, when the destination cannot accept the requested payment rail, and for general invalid parameters. + description: Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` is missing, when the currency is not supported for rule-based accounts, when the account holder has no account in that currency yet, when the destination cannot accept the requested payment rail, and for general invalid parameters. content: application/json: schema: @@ -12037,7 +12038,7 @@ webhooks: ### Event types - - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the customer's account in the same currency the funds were moved to instead: a rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. + - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the account holder's own account in the same currency the funds were moved to instead — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `incomingTransactionId`. operationId: sweepWebhook @@ -12053,7 +12054,7 @@ webhooks: $ref: '#/components/schemas/SweepWebhook' examples: movedToCanonicalAccount: - summary: The amount was below what the corridor carries, so it was moved to the customer's account + summary: The amount was below what the corridor carries, so it was moved to the account holder's own account value: id: Webhook:019542f5-b3e7-1d02-0000-000000000050 type: SWEEP.FAILED @@ -12073,7 +12074,7 @@ webhooks: reason: BELOW_MINIMUM canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 executionFailed: - summary: The sweep was sent but did not go through, so the funds were moved to the customer's account + summary: The sweep was sent but did not go through, so the funds were moved to the account holder's own account value: id: Webhook:019542f5-b3e7-1d02-0000-000000000051 type: SWEEP.FAILED @@ -14985,7 +14986,7 @@ components: - `INTERNAL_FIAT`: A Grid-managed fiat holding account (for example, the USD holding account used as the source for Payouts flows). - `INTERNAL_CRYPTO`: A Grid-managed crypto holding account denominated in a stablecoin such as USDC. - `EMBEDDED_WALLET`: A self-custodial Embedded Wallet provisioned for the customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. - - `RULE_BASED`: An additional account number for an existing customer, with a routing rule attached, so incoming payments can be attributed to a specific payer and swept automatically. Created with `POST /internal-accounts`. + - `RULE_BASED`: An additional account number for an existing account holder, with a routing rule attached, so incoming payments can be attributed to a specific payer and swept automatically. Created with `POST /internal-accounts`. SweepRuleDestinationRequest: title: Sweep Rule Destination Request type: object @@ -15060,7 +15061,7 @@ components: properties: customerId: type: string - description: The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a sweep cannot be completed. + description: 'The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a sweep cannot be completed. Required today: platform-owned rule-based accounts are not yet available.' example: Customer:019542f5-b3e7-1d02-0000-000000000001 type: allOf: @@ -27574,7 +27575,7 @@ components: example: BELOW_MINIMUM canonicalAccountId: type: string - description: The customer's account in the same currency that the funds were moved to. A rule-based account never holds a balance, so a sweep that cannot reach its destination always lands the funds here. + description: The account holder's own account in the same currency that the funds were moved to — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance, so a sweep that cannot reach its destination always lands the funds here. example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 SweepWebhook: allOf: diff --git a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml index 2ff88c1dd..2fd1cd795 100644 --- a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml +++ b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml @@ -15,7 +15,8 @@ properties: description: >- The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a - sweep cannot be completed. + sweep cannot be completed. Required today: platform-owned rule-based + accounts are not yet available. example: Customer:019542f5-b3e7-1d02-0000-000000000001 type: allOf: diff --git a/openapi/components/schemas/customers/InternalAccountType.yaml b/openapi/components/schemas/customers/InternalAccountType.yaml index ad2c47027..18c32fe93 100644 --- a/openapi/components/schemas/customers/InternalAccountType.yaml +++ b/openapi/components/schemas/customers/InternalAccountType.yaml @@ -19,7 +19,7 @@ description: >- customer. Outbound transfers require a session signature produced by the customer's device — see the Embedded Wallets guide. - - `RULE_BASED`: An additional account number for an existing customer, with a - routing rule attached, so incoming payments can be attributed to a specific - payer and swept automatically. Created with + - `RULE_BASED`: An additional account number for an existing account holder, + with a routing rule attached, so incoming payments can be attributed to a + specific payer and swept automatically. Created with `POST /internal-accounts`. diff --git a/openapi/components/schemas/webhooks/SweepFailure.yaml b/openapi/components/schemas/webhooks/SweepFailure.yaml index 578b045db..99aade56d 100644 --- a/openapi/components/schemas/webhooks/SweepFailure.yaml +++ b/openapi/components/schemas/webhooks/SweepFailure.yaml @@ -50,7 +50,8 @@ properties: canonicalAccountId: type: string description: >- - The customer's account in the same currency that the funds were moved to. - A rule-based account never holds a balance, so a sweep that cannot reach - its destination always lands the funds here. + The account holder's own account in the same currency that the funds were + moved to — the customer's, or the platform's for a platform-owned + rule-based account. A rule-based account never holds a balance, so a sweep + that cannot reach its destination always lands the funds here. example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 diff --git a/openapi/paths/internal_accounts.yaml b/openapi/paths/internal_accounts.yaml index 21b7a6ae7..738aba9e4 100644 --- a/openapi/paths/internal_accounts.yaml +++ b/openapi/paths/internal_accounts.yaml @@ -1,21 +1,22 @@ post: summary: Create an internal account description: | - Create a rule-based internal account for an existing customer. + Create a rule-based internal account. - A rule-based account is an additional account number for a customer that - already holds an account in the same currency. It carries a routing rule: - every payment that settles into it is attributed to that account number and - then swept to the rule's destination, so you can issue one number per - payer and reconcile incoming payments without matching on remittance text. + A rule-based account is an additional account number for an account holder + that already holds an account in the same currency. It carries a routing + rule: every payment that settles into it is attributed to that account + number and then swept to the rule's destination, so you can issue one number + per payer and reconcile incoming payments without matching on remittance + text. Only `RULE_BASED` accounts can be created here. `INTERNAL_FIAT`, `INTERNAL_CRYPTO`, and `EMBEDDED_WALLET` accounts are provisioned - automatically when a customer is created or approved. + automatically when an account holder is created or approved. The response carries the account's own `fundingPaymentInstructions` — the routable number payers send to. Rule-based accounts are currently available - in `USD` for individual customers, and must be enabled for your platform + in `USD`, for individual customers, and must be enabled for your platform before this endpoint accepts requests. Sandbox platforms can create rule-based accounts and exercise the whole @@ -97,7 +98,7 @@ post: description: >- Bad request. Returned when `type` is not `RULE_BASED`, when `sweepRule` is missing, when the currency is not supported for rule-based accounts, - when the customer has no account in that currency yet, when the + when the account holder has no account in that currency yet, when the destination cannot accept the requested payment rail, and for general invalid parameters. content: diff --git a/openapi/webhooks/sweep.yaml b/openapi/webhooks/sweep.yaml index d3ec1770f..a59ff5f9d 100644 --- a/openapi/webhooks/sweep.yaml +++ b/openapi/webhooks/sweep.yaml @@ -34,9 +34,10 @@ post: - `SWEEP.FAILED` — Fired when the funds do not reach the rule's - destination. `reason` says why, and `canonicalAccountId` is the customer's - account in the same currency the funds were moved to instead: a rule-based - account never holds a balance. A failed sweep that also created an outgoing + destination. `reason` says why, and `canonicalAccountId` is the account + holder's own account in the same currency the funds were moved to instead — + the customer's, or the platform's for a platform-owned rule-based account. A + rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. @@ -56,7 +57,7 @@ post: $ref: ../components/schemas/webhooks/SweepWebhook.yaml examples: movedToCanonicalAccount: - summary: The amount was below what the corridor carries, so it was moved to the customer's account + summary: The amount was below what the corridor carries, so it was moved to the account holder's own account value: id: Webhook:019542f5-b3e7-1d02-0000-000000000050 type: SWEEP.FAILED @@ -76,7 +77,7 @@ post: reason: BELOW_MINIMUM canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 executionFailed: - summary: The sweep was sent but did not go through, so the funds were moved to the customer's account + summary: The sweep was sent but did not go through, so the funds were moved to the account holder's own account value: id: Webhook:019542f5-b3e7-1d02-0000-000000000051 type: SWEEP.FAILED From dbde6cf403de9ed82e205f55ccbee0473492e076 Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 2 Sep 2026 20:48:19 +0000 Subject: [PATCH 06/16] docs: carry one transaction id on a sweep failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 22 ++++++------------ openapi.yaml | 22 ++++++------------ .../schemas/webhooks/SweepFailure.yaml | 23 +++++++------------ openapi/webhooks/sweep.yaml | 10 ++++---- 4 files changed, 26 insertions(+), 51 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 85f56e835..281eca766 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -12038,9 +12038,9 @@ webhooks: ### Event types - - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the account holder's own account in the same currency the funds were moved to instead — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. + - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the account holder's own account in the same currency the funds were moved to instead — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `transactionId` rather than counting the failure twice. - Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `incomingTransactionId`. + Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `transactionId`. operationId: sweepWebhook tags: - Webhooks @@ -12069,8 +12069,7 @@ webhooks: name: United States Dollar symbol: $ decimals: 2 - incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 - sendTransactionId: null + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 reason: BELOW_MINIMUM canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 executionFailed: @@ -12089,8 +12088,7 @@ webhooks: name: United States Dollar symbol: $ decimals: 2 - incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000012 - sendTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 reason: EXECUTION_FAILED canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 responses: @@ -27542,7 +27540,7 @@ components: - sourceAccountId - destinationAccountId - amount - - incomingTransactionId + - transactionId - reason - canonicalAccountId properties: @@ -27558,16 +27556,10 @@ components: allOf: - $ref: '#/components/schemas/CurrencyAmount' description: The amount that could not be swept. - incomingTransactionId: + transactionId: type: string - description: The transaction that brought the funds into the rule-based account. Delivery of this event is at-least-once, so deduplicate on this value. + description: The operation covering both the deposit into the rule-based account and the sweep out of it. Created when the deposit settles, so it is always present. Delivery of this event is at-least-once, so deduplicate on this value, and use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting the failure twice. example: Transaction:019542f5-b3e7-1d02-0000-000000000010 - sendTransactionId: - type: - - string - - 'null' - description: The operation covering both the deposit into the rule-based account and the sweep out of it, present once the sweep got far enough to create one. Null when the sweep was never attempted. Use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting the failure twice. - example: Transaction:019542f5-b3e7-1d02-0000-000000000011 reason: allOf: - $ref: '#/components/schemas/SweepFailureReason' diff --git a/openapi.yaml b/openapi.yaml index 85f56e835..281eca766 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -12038,9 +12038,9 @@ webhooks: ### Event types - - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the account holder's own account in the same currency the funds were moved to instead — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `sendTransactionId` rather than counting the failure twice. + - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the account holder's own account in the same currency the funds were moved to instead — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `transactionId` rather than counting the failure twice. - Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `incomingTransactionId`. + Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `transactionId`. operationId: sweepWebhook tags: - Webhooks @@ -12069,8 +12069,7 @@ webhooks: name: United States Dollar symbol: $ decimals: 2 - incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 - sendTransactionId: null + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 reason: BELOW_MINIMUM canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 executionFailed: @@ -12089,8 +12088,7 @@ webhooks: name: United States Dollar symbol: $ decimals: 2 - incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000012 - sendTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 reason: EXECUTION_FAILED canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 responses: @@ -27542,7 +27540,7 @@ components: - sourceAccountId - destinationAccountId - amount - - incomingTransactionId + - transactionId - reason - canonicalAccountId properties: @@ -27558,16 +27556,10 @@ components: allOf: - $ref: '#/components/schemas/CurrencyAmount' description: The amount that could not be swept. - incomingTransactionId: + transactionId: type: string - description: The transaction that brought the funds into the rule-based account. Delivery of this event is at-least-once, so deduplicate on this value. + description: The operation covering both the deposit into the rule-based account and the sweep out of it. Created when the deposit settles, so it is always present. Delivery of this event is at-least-once, so deduplicate on this value, and use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting the failure twice. example: Transaction:019542f5-b3e7-1d02-0000-000000000010 - sendTransactionId: - type: - - string - - 'null' - description: The operation covering both the deposit into the rule-based account and the sweep out of it, present once the sweep got far enough to create one. Null when the sweep was never attempted. Use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting the failure twice. - example: Transaction:019542f5-b3e7-1d02-0000-000000000011 reason: allOf: - $ref: '#/components/schemas/SweepFailureReason' diff --git a/openapi/components/schemas/webhooks/SweepFailure.yaml b/openapi/components/schemas/webhooks/SweepFailure.yaml index 99aade56d..f4b536d57 100644 --- a/openapi/components/schemas/webhooks/SweepFailure.yaml +++ b/openapi/components/schemas/webhooks/SweepFailure.yaml @@ -7,7 +7,7 @@ required: - sourceAccountId - destinationAccountId - amount - - incomingTransactionId + - transactionId - reason - canonicalAccountId properties: @@ -25,23 +25,16 @@ properties: allOf: - $ref: ../common/CurrencyAmount.yaml description: The amount that could not be swept. - incomingTransactionId: + transactionId: type: string - description: >- - The transaction that brought the funds into the rule-based account. - Delivery of this event is at-least-once, so deduplicate on this value. - example: Transaction:019542f5-b3e7-1d02-0000-000000000010 - sendTransactionId: - type: - - string - - 'null' description: >- The operation covering both the deposit into the rule-based account and - the sweep out of it, present once the sweep got far enough to create one. - Null when the sweep was never attempted. Use it to correlate this event - with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than - counting the failure twice. - example: Transaction:019542f5-b3e7-1d02-0000-000000000011 + the sweep out of it. Created when the deposit settles, so it is always + present. Delivery of this event is at-least-once, so deduplicate on this + value, and use it to correlate this event with the + `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting + the failure twice. + example: Transaction:019542f5-b3e7-1d02-0000-000000000010 reason: allOf: - $ref: ./SweepFailureReason.yaml diff --git a/openapi/webhooks/sweep.yaml b/openapi/webhooks/sweep.yaml index a59ff5f9d..fdf167b9b 100644 --- a/openapi/webhooks/sweep.yaml +++ b/openapi/webhooks/sweep.yaml @@ -39,11 +39,11 @@ post: the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on - `sendTransactionId` rather than counting the failure twice. + `transactionId` rather than counting the failure twice. Delivery is at-least-once, so the same failure can arrive more than once - with different webhook ids. Deduplicate on `incomingTransactionId`. + with different webhook ids. Deduplicate on `transactionId`. operationId: sweepWebhook tags: - Webhooks @@ -72,8 +72,7 @@ post: name: United States Dollar symbol: $ decimals: 2 - incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 - sendTransactionId: null + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 reason: BELOW_MINIMUM canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 executionFailed: @@ -92,8 +91,7 @@ post: name: United States Dollar symbol: $ decimals: 2 - incomingTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000012 - sendTransactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 reason: EXECUTION_FAILED canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 responses: From eee2a472aaf14d9a6b9bc7f7026bccf2999f804a Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 2 Sep 2026 22:40:10 +0000 Subject: [PATCH 07/16] docs: make customerId optional so the endpoint stays generic 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 --- mintlify/openapi.yaml | 3 +-- openapi.yaml | 3 +-- .../customers/InternalAccountCreateRequest.yaml | 10 +++++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 281eca766..9666a5c17 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -15052,14 +15052,13 @@ components: description: Request body for `POST /internal-accounts`. Only `RULE_BASED` accounts can be created directly, and they always carry a `sweepRule`. additionalProperties: false required: - - customerId - type - currency - sweepRule properties: customerId: type: string - description: 'The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a sweep cannot be completed. Required today: platform-owned rule-based accounts are not yet available.' + 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: diff --git a/openapi.yaml b/openapi.yaml index 281eca766..9666a5c17 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -15052,14 +15052,13 @@ components: description: Request body for `POST /internal-accounts`. Only `RULE_BASED` accounts can be created directly, and they always carry a `sweepRule`. additionalProperties: false required: - - customerId - type - currency - sweepRule properties: customerId: type: string - description: 'The customer the account is created for. The customer must already have a verified account in the same currency, which is where funds land when a sweep cannot be completed. Required today: platform-owned rule-based accounts are not yet available.' + 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: diff --git a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml index 2fd1cd795..6a46ae9fc 100644 --- a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml +++ b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml @@ -5,7 +5,6 @@ description: >- accounts can be created directly, and they always carry a `sweepRule`. additionalProperties: false required: - - customerId - type - currency - sweepRule @@ -13,10 +12,11 @@ properties: customerId: type: string description: >- - The customer the account is created for. The customer must already have a - verified account in the same currency, which is where funds land when a - sweep cannot be completed. Required today: platform-owned rule-based - accounts are not yet available. + 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: From 82beb24bde4c735e9f76ff126a915aa961a12228 Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 2 Sep 2026 23:45:46 +0000 Subject: [PATCH 08/16] docs: carry the sweep outcome on the transaction, not its own webhook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 190 +++--------------- openapi.yaml | 190 +++--------------- .../transactions/IncomingTransaction.yaml | 16 ++ .../SweepFailureReason.yaml | 0 .../schemas/webhooks/SweepFailure.yaml | 50 ----- .../schemas/webhooks/SweepWebhook.yaml | 12 -- .../schemas/webhooks/WebhookType.yaml | 1 - openapi/openapi.yaml | 2 - openapi/webhooks/sweep.yaml | 118 ----------- 9 files changed, 66 insertions(+), 513 deletions(-) rename openapi/components/schemas/{webhooks => transactions}/SweepFailureReason.yaml (100%) delete mode 100644 openapi/components/schemas/webhooks/SweepFailure.yaml delete mode 100644 openapi/components/schemas/webhooks/SweepWebhook.yaml delete mode 100644 openapi/webhooks/sweep.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 9666a5c17..8b3c07b7e 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -12017,102 +12017,6 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' - sweep: - post: - summary: Sweep webhook - description: | - Webhook that is called when a payment settles into a rule-based internal account but cannot be swept to the destination configured on the account's rule. - - This endpoint should be implemented by clients of the Grid API. - - ### Authentication - - The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid. - To verify the signature: - 1. Get the Grid public key provided to you during integration - 2. Decode the base64 signature from the header - 3. Create a SHA-256 hash of the request body - 4. Verify the signature using the public key and the hash - - If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. - - ### Event types - - - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the account holder's own account in the same currency the funds were moved to instead — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `transactionId` rather than counting the failure twice. - - Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `transactionId`. - operationId: sweepWebhook - tags: - - Webhooks - security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SweepWebhook' - examples: - movedToCanonicalAccount: - summary: The amount was below what the corridor carries, so it was moved to the account holder's own account - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000050 - type: SWEEP.FAILED - timestamp: '2026-05-09T10:00:00Z' - data: - sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 - destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: - amount: 250 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 - reason: BELOW_MINIMUM - canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - executionFailed: - summary: The sweep was sent but did not go through, so the funds were moved to the account holder's own account - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000051 - type: SWEEP.FAILED - timestamp: '2026-05-09T10:05:00Z' - data: - sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 - destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: - amount: 12550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 - reason: EXECUTION_FAILED - canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - responses: - '200': - description: | - Webhook received successfully - '400': - description: Bad request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - Signature validation failed - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' components: securitySchemes: BasicAuth: @@ -23397,6 +23301,24 @@ components: - COMPLIANCE_REJECTED - COLLECTION_FAILED description: Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. + SweepFailureReason: + 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. | IncomingTransaction: title: Incoming Transaction allOf: @@ -23427,6 +23349,13 @@ components: failureReason: $ref: '#/components/schemas/IncomingTransactionFailureReason' description: If the transaction failed, this field provides the reason for failure. + sweepFailureReason: + allOf: + - $ref: '#/components/schemas/SweepFailureReason' + description: Set when this payment settled into a rule-based account but was not swept to the destination of the account's rule. The deposit itself succeeded, so the transaction still completes; this says why the destination was not paid. Omitted for any other account. + sweepCanonicalAccountId: + type: string + description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. OutgoingTransactionStatus: type: string enum: @@ -27124,7 +27053,6 @@ components: - CARD_TRANSACTION.EXCEPTION - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED - - SWEEP.FAILED - TEST description: Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot identifies the resource, the part after identifies the event. This lets consumers route purely on type without inspecting data.status. BaseWebhook: @@ -27513,74 +27441,6 @@ components: enum: - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED - SweepFailureReason: - 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. | - SweepFailure: - title: Sweep Failure - type: object - description: A payment that settled into a rule-based account but was not swept to the destination of the account's rule. - required: - - sourceAccountId - - destinationAccountId - - amount - - transactionId - - reason - - canonicalAccountId - properties: - sourceAccountId: - type: string - description: The rule-based internal account the payment settled into. - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 - destinationAccountId: - type: string - description: The destination configured on the account's rule, which the funds did not reach. - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: - allOf: - - $ref: '#/components/schemas/CurrencyAmount' - description: The amount that could not be swept. - transactionId: - type: string - description: The operation covering both the deposit into the rule-based account and the sweep out of it. Created when the deposit settles, so it is always present. Delivery of this event is at-least-once, so deduplicate on this value, and use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting the failure twice. - example: Transaction:019542f5-b3e7-1d02-0000-000000000010 - reason: - allOf: - - $ref: '#/components/schemas/SweepFailureReason' - description: Why the sweep could not be completed. - example: BELOW_MINIMUM - canonicalAccountId: - type: string - description: The account holder's own account in the same currency that the funds were moved to — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance, so a sweep that cannot reach its destination always lands the funds here. - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - SweepWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/SweepFailure' - type: - type: string - enum: - - SWEEP.FAILED requestBodies: DocumentUploadRequestBody: required: true diff --git a/openapi.yaml b/openapi.yaml index 9666a5c17..8b3c07b7e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -12017,102 +12017,6 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' - sweep: - post: - summary: Sweep webhook - description: | - Webhook that is called when a payment settles into a rule-based internal account but cannot be swept to the destination configured on the account's rule. - - This endpoint should be implemented by clients of the Grid API. - - ### Authentication - - The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid. - To verify the signature: - 1. Get the Grid public key provided to you during integration - 2. Decode the base64 signature from the header - 3. Create a SHA-256 hash of the request body - 4. Verify the signature using the public key and the hash - - If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. - - ### Event types - - - `SWEEP.FAILED` — Fired when the funds do not reach the rule's destination. `reason` says why, and `canonicalAccountId` is the account holder's own account in the same currency the funds were moved to instead — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. A failed sweep that also created an outgoing payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on `transactionId` rather than counting the failure twice. - - Delivery is at-least-once, so the same failure can arrive more than once with different webhook ids. Deduplicate on `transactionId`. - operationId: sweepWebhook - tags: - - Webhooks - security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SweepWebhook' - examples: - movedToCanonicalAccount: - summary: The amount was below what the corridor carries, so it was moved to the account holder's own account - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000050 - type: SWEEP.FAILED - timestamp: '2026-05-09T10:00:00Z' - data: - sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 - destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: - amount: 250 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 - reason: BELOW_MINIMUM - canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - executionFailed: - summary: The sweep was sent but did not go through, so the funds were moved to the account holder's own account - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000051 - type: SWEEP.FAILED - timestamp: '2026-05-09T10:05:00Z' - data: - sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 - destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: - amount: 12550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 - reason: EXECUTION_FAILED - canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - responses: - '200': - description: | - Webhook received successfully - '400': - description: Bad request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - Signature validation failed - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' components: securitySchemes: BasicAuth: @@ -23397,6 +23301,24 @@ components: - COMPLIANCE_REJECTED - COLLECTION_FAILED description: Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. + SweepFailureReason: + 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. | IncomingTransaction: title: Incoming Transaction allOf: @@ -23427,6 +23349,13 @@ components: failureReason: $ref: '#/components/schemas/IncomingTransactionFailureReason' description: If the transaction failed, this field provides the reason for failure. + sweepFailureReason: + allOf: + - $ref: '#/components/schemas/SweepFailureReason' + description: Set when this payment settled into a rule-based account but was not swept to the destination of the account's rule. The deposit itself succeeded, so the transaction still completes; this says why the destination was not paid. Omitted for any other account. + sweepCanonicalAccountId: + type: string + description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. OutgoingTransactionStatus: type: string enum: @@ -27124,7 +27053,6 @@ components: - CARD_TRANSACTION.EXCEPTION - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED - - SWEEP.FAILED - TEST description: Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot identifies the resource, the part after identifies the event. This lets consumers route purely on type without inspecting data.status. BaseWebhook: @@ -27513,74 +27441,6 @@ components: enum: - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED - SweepFailureReason: - 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. | - SweepFailure: - title: Sweep Failure - type: object - description: A payment that settled into a rule-based account but was not swept to the destination of the account's rule. - required: - - sourceAccountId - - destinationAccountId - - amount - - transactionId - - reason - - canonicalAccountId - properties: - sourceAccountId: - type: string - description: The rule-based internal account the payment settled into. - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 - destinationAccountId: - type: string - description: The destination configured on the account's rule, which the funds did not reach. - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: - allOf: - - $ref: '#/components/schemas/CurrencyAmount' - description: The amount that could not be swept. - transactionId: - type: string - description: The operation covering both the deposit into the rule-based account and the sweep out of it. Created when the deposit settles, so it is always present. Delivery of this event is at-least-once, so deduplicate on this value, and use it to correlate this event with the `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting the failure twice. - example: Transaction:019542f5-b3e7-1d02-0000-000000000010 - reason: - allOf: - - $ref: '#/components/schemas/SweepFailureReason' - description: Why the sweep could not be completed. - example: BELOW_MINIMUM - canonicalAccountId: - type: string - description: The account holder's own account in the same currency that the funds were moved to — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance, so a sweep that cannot reach its destination always lands the funds here. - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - SweepWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/SweepFailure' - type: - type: string - enum: - - SWEEP.FAILED requestBodies: DocumentUploadRequestBody: required: true diff --git a/openapi/components/schemas/transactions/IncomingTransaction.yaml b/openapi/components/schemas/transactions/IncomingTransaction.yaml index 2fa8b000d..f619509e3 100644 --- a/openapi/components/schemas/transactions/IncomingTransaction.yaml +++ b/openapi/components/schemas/transactions/IncomingTransaction.yaml @@ -29,3 +29,19 @@ allOf: failureReason: $ref: ./IncomingTransactionFailureReason.yaml description: If the transaction failed, this field provides the reason for failure. + sweepFailureReason: + allOf: + - $ref: ./SweepFailureReason.yaml + description: >- + Set when this payment settled into a rule-based account but was not + swept to the destination of the account's rule. The deposit itself + succeeded, so the transaction still completes; this says why the + destination was not paid. Omitted for any other account. + sweepCanonicalAccountId: + type: string + description: >- + The account holder's own account in the same currency the funds were + moved to when a sweep could not be completed — the customer's, or the + platform's for a platform-owned rule-based account. A rule-based + account never holds a balance. Present whenever + `sweepFailureReason` is. diff --git a/openapi/components/schemas/webhooks/SweepFailureReason.yaml b/openapi/components/schemas/transactions/SweepFailureReason.yaml similarity index 100% rename from openapi/components/schemas/webhooks/SweepFailureReason.yaml rename to openapi/components/schemas/transactions/SweepFailureReason.yaml diff --git a/openapi/components/schemas/webhooks/SweepFailure.yaml b/openapi/components/schemas/webhooks/SweepFailure.yaml deleted file mode 100644 index f4b536d57..000000000 --- a/openapi/components/schemas/webhooks/SweepFailure.yaml +++ /dev/null @@ -1,50 +0,0 @@ -title: Sweep Failure -type: object -description: >- - A payment that settled into a rule-based account but was not swept to the - destination of the account's rule. -required: - - sourceAccountId - - destinationAccountId - - amount - - transactionId - - reason - - canonicalAccountId -properties: - sourceAccountId: - type: string - description: The rule-based internal account the payment settled into. - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 - destinationAccountId: - type: string - description: >- - The destination configured on the account's rule, which the funds did not - reach. - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: - allOf: - - $ref: ../common/CurrencyAmount.yaml - description: The amount that could not be swept. - transactionId: - type: string - description: >- - The operation covering both the deposit into the rule-based account and - the sweep out of it. Created when the deposit settles, so it is always - present. Delivery of this event is at-least-once, so deduplicate on this - value, and use it to correlate this event with the - `OUTGOING_PAYMENT.FAILED` event for the same sweep rather than counting - the failure twice. - example: Transaction:019542f5-b3e7-1d02-0000-000000000010 - reason: - allOf: - - $ref: ./SweepFailureReason.yaml - description: Why the sweep could not be completed. - example: BELOW_MINIMUM - canonicalAccountId: - type: string - description: >- - The account holder's own account in the same currency that the funds were - moved to — the customer's, or the platform's for a platform-owned - rule-based account. A rule-based account never holds a balance, so a sweep - that cannot reach its destination always lands the funds here. - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 diff --git a/openapi/components/schemas/webhooks/SweepWebhook.yaml b/openapi/components/schemas/webhooks/SweepWebhook.yaml deleted file mode 100644 index 4b7df27d8..000000000 --- a/openapi/components/schemas/webhooks/SweepWebhook.yaml +++ /dev/null @@ -1,12 +0,0 @@ -allOf: - - $ref: ./BaseWebhook.yaml - - type: object - required: - - data - properties: - data: - $ref: ./SweepFailure.yaml - type: - type: string - enum: - - SWEEP.FAILED diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml index 2e3430531..0d7da939d 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -45,7 +45,6 @@ enum: - CARD_TRANSACTION.EXCEPTION - WALLET_OPERATION.COMPLETED - WALLET_OPERATION.FAILED - - SWEEP.FAILED - TEST description: >- Type of webhook event in OBJECT.EVENT dot-notation. The part before the dot diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index de3242f66..de980b707 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -415,8 +415,6 @@ webhooks: $ref: webhooks/card-transaction.yaml wallet-operation: $ref: webhooks/wallet-operation.yaml - sweep: - $ref: webhooks/sweep.yaml security: - BasicAuth: [] - AgentAuth: [] diff --git a/openapi/webhooks/sweep.yaml b/openapi/webhooks/sweep.yaml deleted file mode 100644 index fdf167b9b..000000000 --- a/openapi/webhooks/sweep.yaml +++ /dev/null @@ -1,118 +0,0 @@ -post: - summary: Sweep webhook - description: > - Webhook that is called when a payment settles into a rule-based internal - account but cannot be swept to the destination configured on the - account's rule. - - - This endpoint should be implemented by clients of the Grid API. - - - ### Authentication - - - The webhook includes a signature in the `X-Grid-Signature` header that - allows you to verify that the webhook was sent by Grid. - - To verify the signature: - - 1. Get the Grid public key provided to you during integration - - 2. Decode the base64 signature from the header - - 3. Create a SHA-256 hash of the request body - - 4. Verify the signature using the public key and the hash - - - If the signature verification succeeds, the webhook is authentic. If not, it - should be rejected. - - - ### Event types - - - - `SWEEP.FAILED` — Fired when the funds do not reach the rule's - destination. `reason` says why, and `canonicalAccountId` is the account - holder's own account in the same currency the funds were moved to instead — - the customer's, or the platform's for a platform-owned rule-based account. A - rule-based account never holds a balance. A failed sweep that also created an outgoing - payment fires `OUTGOING_PAYMENT.FAILED` as well; correlate the two on - `transactionId` rather than counting the failure twice. - - - Delivery is at-least-once, so the same failure can arrive more than once - with different webhook ids. Deduplicate on `transactionId`. - operationId: sweepWebhook - tags: - - Webhooks - security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../components/schemas/webhooks/SweepWebhook.yaml - examples: - movedToCanonicalAccount: - summary: The amount was below what the corridor carries, so it was moved to the account holder's own account - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000050 - type: SWEEP.FAILED - timestamp: '2026-05-09T10:00:00Z' - data: - sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 - destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: - amount: 250 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000010 - reason: BELOW_MINIMUM - canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - executionFailed: - summary: The sweep was sent but did not go through, so the funds were moved to the account holder's own account - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000051 - type: SWEEP.FAILED - timestamp: '2026-05-09T10:05:00Z' - data: - sourceAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000005 - destinationAccountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: - amount: 12550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000013 - reason: EXECUTION_FAILED - canonicalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000006 - responses: - '200': - description: > - Webhook received successfully - '400': - description: Bad request - content: - application/json: - schema: - $ref: ../components/schemas/errors/Error400.yaml - '401': - description: Unauthorized - Signature validation failed - content: - application/json: - schema: - $ref: ../components/schemas/errors/Error401.yaml - '409': - description: Conflict - Webhook has already been processed (duplicate id) - content: - application/json: - schema: - $ref: ../components/schemas/errors/Error409.yaml From d170b60aa648f70a11eadb6b6f7915d0b505f6e3 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 3 Sep 2026 20:42:09 +0000 Subject: [PATCH 09/16] docs: let a transaction exist before its amount is priced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 10 ++++++++-- openapi.yaml | 10 ++++++++-- .../transactions/IncomingTransaction.yaml | 6 ++++-- .../transactions/OutgoingTransaction.yaml | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 8b3c07b7e..3c0fe3b4c 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -23326,7 +23326,6 @@ components: - type: object required: - type - - receivedAmount properties: type: type: string @@ -23336,7 +23335,7 @@ components: $ref: '#/components/schemas/TransactionSourceOneOf' receivedAmount: $ref: '#/components/schemas/CurrencyAmount' - description: Amount received in the recipient's currency + description: Amount received in the recipient's currency. Absent until the amount is priced — a payment swept out of a rule-based account is created when the deposit settles, before its quote locks a rate. fees: type: integer format: int64 @@ -23542,6 +23541,13 @@ components: failureReason: $ref: '#/components/schemas/OutgoingTransactionFailureReason' description: If the transaction failed, this field provides the reason for failure. + sweepFailureReason: + allOf: + - $ref: '#/components/schemas/SweepFailureReason' + description: Set when this payment settled into a rule-based account but was not swept to the destination of the account's rule. The deposit itself succeeded, so the transaction still completes; this says why the destination was not paid. Omitted for any other account. + sweepCanonicalAccountId: + type: string + description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. paymentRail: anyOf: - $ref: '#/components/schemas/PaymentRail' diff --git a/openapi.yaml b/openapi.yaml index 8b3c07b7e..3c0fe3b4c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -23326,7 +23326,6 @@ components: - type: object required: - type - - receivedAmount properties: type: type: string @@ -23336,7 +23335,7 @@ components: $ref: '#/components/schemas/TransactionSourceOneOf' receivedAmount: $ref: '#/components/schemas/CurrencyAmount' - description: Amount received in the recipient's currency + description: Amount received in the recipient's currency. Absent until the amount is priced — a payment swept out of a rule-based account is created when the deposit settles, before its quote locks a rate. fees: type: integer format: int64 @@ -23542,6 +23541,13 @@ components: failureReason: $ref: '#/components/schemas/OutgoingTransactionFailureReason' description: If the transaction failed, this field provides the reason for failure. + sweepFailureReason: + allOf: + - $ref: '#/components/schemas/SweepFailureReason' + description: Set when this payment settled into a rule-based account but was not swept to the destination of the account's rule. The deposit itself succeeded, so the transaction still completes; this says why the destination was not paid. Omitted for any other account. + sweepCanonicalAccountId: + type: string + description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. paymentRail: anyOf: - $ref: '#/components/schemas/PaymentRail' diff --git a/openapi/components/schemas/transactions/IncomingTransaction.yaml b/openapi/components/schemas/transactions/IncomingTransaction.yaml index f619509e3..a5d8a991f 100644 --- a/openapi/components/schemas/transactions/IncomingTransaction.yaml +++ b/openapi/components/schemas/transactions/IncomingTransaction.yaml @@ -4,7 +4,6 @@ allOf: - type: object required: - type - - receivedAmount properties: type: type: string @@ -14,7 +13,10 @@ allOf: $ref: ./TransactionSourceOneOf.yaml receivedAmount: $ref: ../common/CurrencyAmount.yaml - description: Amount received in the recipient's currency + description: >- + Amount received in the recipient's currency. Absent until the amount + is priced — a payment swept out of a rule-based account is created + when the deposit settles, before its quote locks a rate. fees: type: integer format: int64 diff --git a/openapi/components/schemas/transactions/OutgoingTransaction.yaml b/openapi/components/schemas/transactions/OutgoingTransaction.yaml index 19919cc2e..9e652a55e 100644 --- a/openapi/components/schemas/transactions/OutgoingTransaction.yaml +++ b/openapi/components/schemas/transactions/OutgoingTransaction.yaml @@ -71,6 +71,22 @@ allOf: failureReason: $ref: ./OutgoingTransactionFailureReason.yaml description: If the transaction failed, this field provides the reason for failure. + sweepFailureReason: + allOf: + - $ref: ./SweepFailureReason.yaml + description: >- + Set when this payment settled into a rule-based account but was not + swept to the destination of the account's rule. The deposit itself + succeeded, so the transaction still completes; this says why the + destination was not paid. Omitted for any other account. + sweepCanonicalAccountId: + type: string + description: >- + The account holder's own account in the same currency the funds were + moved to when a sweep could not be completed — the customer's, or the + platform's for a platform-owned rule-based account. A rule-based + account never holds a balance. Present whenever + `sweepFailureReason` is. paymentRail: anyOf: - $ref: ../common/PaymentRail.yaml From 906870894c2af88c9ed7218819f8bac8a7bbf90b Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 3 Sep 2026 21:05:03 +0000 Subject: [PATCH 10/16] docs: make sweepRule optional and pin the corridor bound currency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 9 ++++----- openapi.yaml | 9 ++++----- .../InternalAccountCreateRequest.yaml | 10 ++++++---- .../schemas/customers/SweepRule.yaml | 18 +++++++++--------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 3c0fe3b4c..48ddb3c10 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -14953,12 +14953,11 @@ components: InternalAccountCreateRequest: 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`. + description: Request body for `POST /internal-accounts`. Only `RULE_BASED` accounts can be created today, and they are rejected without a `sweepRule` — the type and its rule are created together or not at all. additionalProperties: false required: - type - currency - - sweepRule properties: customerId: type: string @@ -14981,7 +14980,7 @@ components: sweepRule: allOf: - $ref: '#/components/schemas/SweepRuleRequest' - description: The routing rule for the account. Required when `type` is `RULE_BASED`. + description: 'The routing rule for the account. Required when `type` is `RULE_BASED`, which is the only type this endpoint creates today: a rule-based account with no rule has no meaning, so the two are written together.' InternalAccountStatus: title: Internal Account Status type: string @@ -17783,11 +17782,11 @@ components: minimumAmount: allOf: - $ref: '#/components/schemas/CurrencyAmount' - 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 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.' + description: '**In this rule-based account''s currency, not the destination''s.** The smallest balance the corridor to the destination can carry; a settled balance below it is not swept. Zero means no floor, which is the case for a same-currency internal destination — a book transfer with no rail, fee or conversion to justify one. Configuration rather than a moving estimate, so there is nothing to re-poll.' maximumAmount: allOf: - $ref: '#/components/schemas/CurrencyAmount' - description: The largest balance the corridor to the destination can carry. Null means no ceiling applies. A settled balance above it is not swept. + description: '**In this rule-based account''s currency, not the destination''s.** The largest balance the corridor to the destination can carry; a settled balance above it is not swept. Null means no ceiling.' purposeOfPayment: allOf: - $ref: '#/components/schemas/PurposeOfPayment' diff --git a/openapi.yaml b/openapi.yaml index 3c0fe3b4c..48ddb3c10 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -14953,12 +14953,11 @@ components: InternalAccountCreateRequest: 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`. + description: Request body for `POST /internal-accounts`. Only `RULE_BASED` accounts can be created today, and they are rejected without a `sweepRule` — the type and its rule are created together or not at all. additionalProperties: false required: - type - currency - - sweepRule properties: customerId: type: string @@ -14981,7 +14980,7 @@ components: sweepRule: allOf: - $ref: '#/components/schemas/SweepRuleRequest' - description: The routing rule for the account. Required when `type` is `RULE_BASED`. + description: 'The routing rule for the account. Required when `type` is `RULE_BASED`, which is the only type this endpoint creates today: a rule-based account with no rule has no meaning, so the two are written together.' InternalAccountStatus: title: Internal Account Status type: string @@ -17783,11 +17782,11 @@ components: minimumAmount: allOf: - $ref: '#/components/schemas/CurrencyAmount' - 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 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.' + description: '**In this rule-based account''s currency, not the destination''s.** The smallest balance the corridor to the destination can carry; a settled balance below it is not swept. Zero means no floor, which is the case for a same-currency internal destination — a book transfer with no rail, fee or conversion to justify one. Configuration rather than a moving estimate, so there is nothing to re-poll.' maximumAmount: allOf: - $ref: '#/components/schemas/CurrencyAmount' - description: The largest balance the corridor to the destination can carry. Null means no ceiling applies. A settled balance above it is not swept. + description: '**In this rule-based account''s currency, not the destination''s.** The largest balance the corridor to the destination can carry; a settled balance above it is not swept. Null means no ceiling.' purposeOfPayment: allOf: - $ref: '#/components/schemas/PurposeOfPayment' diff --git a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml index 6a46ae9fc..b1c7bfb2c 100644 --- a/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml +++ b/openapi/components/schemas/customers/InternalAccountCreateRequest.yaml @@ -1,13 +1,13 @@ 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`. + Request body for `POST /internal-accounts`. Only `RULE_BASED` accounts can be + created today, and they are rejected without a `sweepRule` — the type and its + rule are created together or not at all. additionalProperties: false required: - type - currency - - sweepRule properties: customerId: type: string @@ -43,4 +43,6 @@ properties: allOf: - $ref: ./SweepRuleRequest.yaml description: >- - The routing rule for the account. Required when `type` is `RULE_BASED`. + The routing rule for the account. Required when `type` is `RULE_BASED`, + which is the only type this endpoint creates today: a rule-based account + with no rule has no meaning, so the two are written together. diff --git a/openapi/components/schemas/customers/SweepRule.yaml b/openapi/components/schemas/customers/SweepRule.yaml index ebb9f8866..9f08c30db 100644 --- a/openapi/components/schemas/customers/SweepRule.yaml +++ b/openapi/components/schemas/customers/SweepRule.yaml @@ -15,19 +15,19 @@ properties: 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 - 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. + **In this rule-based account's currency, not the destination's.** The + smallest balance the corridor to the destination can carry; a settled + balance below it is not swept. Zero means no floor, which is the case for + a same-currency internal destination — a book transfer with no rail, fee + or conversion to justify one. Configuration rather than a moving + estimate, 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. + **In this rule-based account's currency, not the destination's.** The + largest balance the corridor to the destination can carry; a settled + balance above it is not swept. Null means no ceiling. purposeOfPayment: allOf: - $ref: ../quotes/PurposeOfPayment.yaml From d036f666cf9447d9233176c62d55bc70b2ae6e26 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 3 Sep 2026 21:08:59 +0000 Subject: [PATCH 11/16] docs: say how sweepFailureReason and failureReason relate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 18 +++++++++++---- openapi.yaml | 18 +++++++++++---- .../transactions/IncomingTransaction.yaml | 23 +++++++++++++++---- .../transactions/OutgoingTransaction.yaml | 23 +++++++++++++++---- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 48ddb3c10..4955cc4c2 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -23346,11 +23346,16 @@ components: description: Included for all transactions except those with "CREATED" status failureReason: $ref: '#/components/schemas/IncomingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. + description: If the transaction failed, this field provides the reason for failure. Set only when `status` is `FAILED`. A rule-based account whose sweep was refused is not a failure of this transaction — see `sweepFailureReason`. sweepFailureReason: allOf: - $ref: '#/components/schemas/SweepFailureReason' - description: Set when this payment settled into a rule-based account but was not swept to the destination of the account's rule. The deposit itself succeeded, so the transaction still completes; this says why the destination was not paid. Omitted for any other account. + description: |- + Why the onward sweep did not reach the rule's destination, when this payment settled into a rule-based account. + + Distinct from `failureReason`, and never set at the same time: the two describe different events and are told apart by `status`. This transaction **succeeded** — the deposit landed and `status` is `COMPLETED`; only the sweep out of the account was refused, and the funds are in `sweepCanonicalAccountId`. `failureReason` appears only when `status` is `FAILED`, meaning the payment itself did not credit. + + Omitted for any account that is not rule-based. sweepCanonicalAccountId: type: string description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. @@ -23539,11 +23544,16 @@ components: description: Details about the rate and fees for the transaction. failureReason: $ref: '#/components/schemas/OutgoingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. + description: If the transaction failed, this field provides the reason for failure. Set only when `status` is `FAILED`. A rule-based account whose sweep was refused is not a failure of this transaction — see `sweepFailureReason`. sweepFailureReason: allOf: - $ref: '#/components/schemas/SweepFailureReason' - description: Set when this payment settled into a rule-based account but was not swept to the destination of the account's rule. The deposit itself succeeded, so the transaction still completes; this says why the destination was not paid. Omitted for any other account. + description: |- + Why the onward sweep did not reach the rule's destination, when this payment settled into a rule-based account. + + Distinct from `failureReason`, and never set at the same time: the two describe different events and are told apart by `status`. This transaction **succeeded** — the deposit landed and `status` is `COMPLETED`; only the sweep out of the account was refused, and the funds are in `sweepCanonicalAccountId`. `failureReason` appears only when `status` is `FAILED`, meaning the payment itself did not credit. + + Omitted for any account that is not rule-based. sweepCanonicalAccountId: type: string description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. diff --git a/openapi.yaml b/openapi.yaml index 48ddb3c10..4955cc4c2 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -23346,11 +23346,16 @@ components: description: Included for all transactions except those with "CREATED" status failureReason: $ref: '#/components/schemas/IncomingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. + description: If the transaction failed, this field provides the reason for failure. Set only when `status` is `FAILED`. A rule-based account whose sweep was refused is not a failure of this transaction — see `sweepFailureReason`. sweepFailureReason: allOf: - $ref: '#/components/schemas/SweepFailureReason' - description: Set when this payment settled into a rule-based account but was not swept to the destination of the account's rule. The deposit itself succeeded, so the transaction still completes; this says why the destination was not paid. Omitted for any other account. + description: |- + Why the onward sweep did not reach the rule's destination, when this payment settled into a rule-based account. + + Distinct from `failureReason`, and never set at the same time: the two describe different events and are told apart by `status`. This transaction **succeeded** — the deposit landed and `status` is `COMPLETED`; only the sweep out of the account was refused, and the funds are in `sweepCanonicalAccountId`. `failureReason` appears only when `status` is `FAILED`, meaning the payment itself did not credit. + + Omitted for any account that is not rule-based. sweepCanonicalAccountId: type: string description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. @@ -23539,11 +23544,16 @@ components: description: Details about the rate and fees for the transaction. failureReason: $ref: '#/components/schemas/OutgoingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. + description: If the transaction failed, this field provides the reason for failure. Set only when `status` is `FAILED`. A rule-based account whose sweep was refused is not a failure of this transaction — see `sweepFailureReason`. sweepFailureReason: allOf: - $ref: '#/components/schemas/SweepFailureReason' - description: Set when this payment settled into a rule-based account but was not swept to the destination of the account's rule. The deposit itself succeeded, so the transaction still completes; this says why the destination was not paid. Omitted for any other account. + description: |- + Why the onward sweep did not reach the rule's destination, when this payment settled into a rule-based account. + + Distinct from `failureReason`, and never set at the same time: the two describe different events and are told apart by `status`. This transaction **succeeded** — the deposit landed and `status` is `COMPLETED`; only the sweep out of the account was refused, and the funds are in `sweepCanonicalAccountId`. `failureReason` appears only when `status` is `FAILED`, meaning the payment itself did not credit. + + Omitted for any account that is not rule-based. sweepCanonicalAccountId: type: string description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. diff --git a/openapi/components/schemas/transactions/IncomingTransaction.yaml b/openapi/components/schemas/transactions/IncomingTransaction.yaml index a5d8a991f..750ed82ef 100644 --- a/openapi/components/schemas/transactions/IncomingTransaction.yaml +++ b/openapi/components/schemas/transactions/IncomingTransaction.yaml @@ -30,15 +30,28 @@ allOf: description: Included for all transactions except those with "CREATED" status failureReason: $ref: ./IncomingTransactionFailureReason.yaml - description: If the transaction failed, this field provides the reason for failure. + description: >- + If the transaction failed, this field provides the reason for failure. + Set only when `status` is `FAILED`. A rule-based account whose sweep + was refused is not a failure of this transaction — see + `sweepFailureReason`. sweepFailureReason: allOf: - $ref: ./SweepFailureReason.yaml description: >- - Set when this payment settled into a rule-based account but was not - swept to the destination of the account's rule. The deposit itself - succeeded, so the transaction still completes; this says why the - destination was not paid. Omitted for any other account. + Why the onward sweep did not reach the rule's destination, when this + payment settled into a rule-based account. + + + Distinct from `failureReason`, and never set at the same time: the two + describe different events and are told apart by `status`. This + transaction **succeeded** — the deposit landed and `status` is + `COMPLETED`; only the sweep out of the account was refused, and the + funds are in `sweepCanonicalAccountId`. `failureReason` appears only + when `status` is `FAILED`, meaning the payment itself did not credit. + + + Omitted for any account that is not rule-based. sweepCanonicalAccountId: type: string description: >- diff --git a/openapi/components/schemas/transactions/OutgoingTransaction.yaml b/openapi/components/schemas/transactions/OutgoingTransaction.yaml index 9e652a55e..b80b75fd9 100644 --- a/openapi/components/schemas/transactions/OutgoingTransaction.yaml +++ b/openapi/components/schemas/transactions/OutgoingTransaction.yaml @@ -70,15 +70,28 @@ allOf: description: Details about the rate and fees for the transaction. failureReason: $ref: ./OutgoingTransactionFailureReason.yaml - description: If the transaction failed, this field provides the reason for failure. + description: >- + If the transaction failed, this field provides the reason for failure. + Set only when `status` is `FAILED`. A rule-based account whose sweep + was refused is not a failure of this transaction — see + `sweepFailureReason`. sweepFailureReason: allOf: - $ref: ./SweepFailureReason.yaml description: >- - Set when this payment settled into a rule-based account but was not - swept to the destination of the account's rule. The deposit itself - succeeded, so the transaction still completes; this says why the - destination was not paid. Omitted for any other account. + Why the onward sweep did not reach the rule's destination, when this + payment settled into a rule-based account. + + + Distinct from `failureReason`, and never set at the same time: the two + describe different events and are told apart by `status`. This + transaction **succeeded** — the deposit landed and `status` is + `COMPLETED`; only the sweep out of the account was refused, and the + funds are in `sweepCanonicalAccountId`. `failureReason` appears only + when `status` is `FAILED`, meaning the payment itself did not credit. + + + Omitted for any account that is not rule-based. sweepCanonicalAccountId: type: string description: >- From 004fba4aaf9d8a97ce6b6d8705e54863736a1b36 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 3 Sep 2026 21:13:45 +0000 Subject: [PATCH 12/16] docs: fail the transaction when its sweep does not complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-Authored-By: bsiaotickchong --- mintlify/openapi.yaml | 72 +++++++------------ openapi.yaml | 72 +++++++------------ .../transactions/IncomingTransaction.yaml | 38 ++-------- .../IncomingTransactionFailureReason.yaml | 15 ++++ .../transactions/OutgoingTransaction.yaml | 31 +------- .../OutgoingTransactionFailureReason.yaml | 15 ++++ .../transactions/SweepFailureReason.yaml | 17 ----- 7 files changed, 92 insertions(+), 168 deletions(-) delete mode 100644 openapi/components/schemas/transactions/SweepFailureReason.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 4955cc4c2..40c68c344 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -23299,25 +23299,14 @@ components: - QUOTE_EXECUTION_FAILED - COMPLIANCE_REJECTED - COLLECTION_FAILED - description: Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. - SweepFailureReason: - 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. | + - SWEEP_BELOW_MINIMUM + - SWEEP_ABOVE_MAXIMUM + - SWEEP_NO_ELIGIBLE_RAIL + - SWEEP_QUOTE_FAILED + - SWEEP_EXECUTION_FAILED + description: |- + Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. + A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_BELOW_MINIMUM and SWEEP_ABOVE_MAXIMUM mean the balance was outside the band the corridor to the destination can carry. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available to the destination could carry it. SWEEP_QUOTE_FAILED means Grid could not price it, and SWEEP_EXECUTION_FAILED that it was priced but the resulting payment did not go through. IncomingTransaction: title: Incoming Transaction allOf: @@ -23334,7 +23323,7 @@ components: $ref: '#/components/schemas/TransactionSourceOneOf' receivedAmount: $ref: '#/components/schemas/CurrencyAmount' - description: Amount received in the recipient's currency. Absent until the amount is priced — a payment swept out of a rule-based account is created when the deposit settles, before its quote locks a rate. + description: 'Amount received in the recipient''s currency. Always present, except on a rule-based account sweep that was never priced: that transaction is created when the deposit settles, so a sweep failing before its quote locks a rate has no received amount to report.' fees: type: integer format: int64 @@ -23346,19 +23335,7 @@ components: description: Included for all transactions except those with "CREATED" status failureReason: $ref: '#/components/schemas/IncomingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. Set only when `status` is `FAILED`. A rule-based account whose sweep was refused is not a failure of this transaction — see `sweepFailureReason`. - sweepFailureReason: - allOf: - - $ref: '#/components/schemas/SweepFailureReason' - description: |- - Why the onward sweep did not reach the rule's destination, when this payment settled into a rule-based account. - - Distinct from `failureReason`, and never set at the same time: the two describe different events and are told apart by `status`. This transaction **succeeded** — the deposit landed and `status` is `COMPLETED`; only the sweep out of the account was refused, and the funds are in `sweepCanonicalAccountId`. `failureReason` appears only when `status` is `FAILED`, meaning the payment itself did not credit. - - Omitted for any account that is not rule-based. - sweepCanonicalAccountId: - type: string - description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. + description: If the transaction failed, this field provides the reason for failure. A payment that settled into a rule-based account but could not be swept to the rule's destination fails here too, with one of the `SWEEP_*` reasons. OutgoingTransactionStatus: type: string enum: @@ -23441,6 +23418,11 @@ components: - COMPLIANCE_REJECTED - LIGHTNING_PAYMENT_FAILED - COUNTERPARTY_POST_TX_FAILED + - SWEEP_BELOW_MINIMUM + - SWEEP_ABOVE_MAXIMUM + - SWEEP_NO_ELIGIBLE_RAIL + - SWEEP_QUOTE_FAILED + - SWEEP_EXECUTION_FAILED description: | Reason for failure of an outgoing transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed @@ -23472,6 +23454,16 @@ components: | `COMPLIANCE_REJECTED` | The payout partner rejected the recipient on compliance grounds — sanctions, watchlist, or KYC/AML screening | | `LIGHTNING_PAYMENT_FAILED` | Deprecated — superseded by `QUOTE_EXECUTION_FAILED`. The Lightning settlement leg failed | | `COUNTERPARTY_POST_TX_FAILED` | Deprecated — coarse fallback retained for historical transactions when no granular payout reason is available | + | `SWEEP_BELOW_MINIMUM` | The payment settled into a rule-based account but was below the smallest amount the corridor to the rule's destination can carry | + | `SWEEP_ABOVE_MAXIMUM` | The payment settled into a rule-based account but was above the largest amount the corridor to the rule's destination can carry | + | `SWEEP_NO_ELIGIBLE_RAIL` | No payment rail available to the rule's destination could carry the sweep | + | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination | + | `SWEEP_EXECUTION_FAILED` | The sweep was priced but the resulting payment did not go through | + + The `SWEEP_*` reasons apply to a payment that settled into a rule-based + account and could not be swept onward. A rule-based account never holds a + balance, so the funds are moved to the account holder's own account in the + same currency. RailSelectionMode: type: string enum: @@ -23544,19 +23536,7 @@ components: description: Details about the rate and fees for the transaction. failureReason: $ref: '#/components/schemas/OutgoingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. Set only when `status` is `FAILED`. A rule-based account whose sweep was refused is not a failure of this transaction — see `sweepFailureReason`. - sweepFailureReason: - allOf: - - $ref: '#/components/schemas/SweepFailureReason' - description: |- - Why the onward sweep did not reach the rule's destination, when this payment settled into a rule-based account. - - Distinct from `failureReason`, and never set at the same time: the two describe different events and are told apart by `status`. This transaction **succeeded** — the deposit landed and `status` is `COMPLETED`; only the sweep out of the account was refused, and the funds are in `sweepCanonicalAccountId`. `failureReason` appears only when `status` is `FAILED`, meaning the payment itself did not credit. - - Omitted for any account that is not rule-based. - sweepCanonicalAccountId: - type: string - description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. + description: If the transaction failed, this field provides the reason for failure. A payment that settled into a rule-based account but could not be swept to the rule's destination fails here too, with one of the `SWEEP_*` reasons. paymentRail: anyOf: - $ref: '#/components/schemas/PaymentRail' diff --git a/openapi.yaml b/openapi.yaml index 4955cc4c2..40c68c344 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -23299,25 +23299,14 @@ components: - QUOTE_EXECUTION_FAILED - COMPLIANCE_REJECTED - COLLECTION_FAILED - description: Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. - SweepFailureReason: - 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. | + - SWEEP_BELOW_MINIMUM + - SWEEP_ABOVE_MAXIMUM + - SWEEP_NO_ELIGIBLE_RAIL + - SWEEP_QUOTE_FAILED + - SWEEP_EXECUTION_FAILED + description: |- + Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. + A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_BELOW_MINIMUM and SWEEP_ABOVE_MAXIMUM mean the balance was outside the band the corridor to the destination can carry. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available to the destination could carry it. SWEEP_QUOTE_FAILED means Grid could not price it, and SWEEP_EXECUTION_FAILED that it was priced but the resulting payment did not go through. IncomingTransaction: title: Incoming Transaction allOf: @@ -23334,7 +23323,7 @@ components: $ref: '#/components/schemas/TransactionSourceOneOf' receivedAmount: $ref: '#/components/schemas/CurrencyAmount' - description: Amount received in the recipient's currency. Absent until the amount is priced — a payment swept out of a rule-based account is created when the deposit settles, before its quote locks a rate. + description: 'Amount received in the recipient''s currency. Always present, except on a rule-based account sweep that was never priced: that transaction is created when the deposit settles, so a sweep failing before its quote locks a rate has no received amount to report.' fees: type: integer format: int64 @@ -23346,19 +23335,7 @@ components: description: Included for all transactions except those with "CREATED" status failureReason: $ref: '#/components/schemas/IncomingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. Set only when `status` is `FAILED`. A rule-based account whose sweep was refused is not a failure of this transaction — see `sweepFailureReason`. - sweepFailureReason: - allOf: - - $ref: '#/components/schemas/SweepFailureReason' - description: |- - Why the onward sweep did not reach the rule's destination, when this payment settled into a rule-based account. - - Distinct from `failureReason`, and never set at the same time: the two describe different events and are told apart by `status`. This transaction **succeeded** — the deposit landed and `status` is `COMPLETED`; only the sweep out of the account was refused, and the funds are in `sweepCanonicalAccountId`. `failureReason` appears only when `status` is `FAILED`, meaning the payment itself did not credit. - - Omitted for any account that is not rule-based. - sweepCanonicalAccountId: - type: string - description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. + description: If the transaction failed, this field provides the reason for failure. A payment that settled into a rule-based account but could not be swept to the rule's destination fails here too, with one of the `SWEEP_*` reasons. OutgoingTransactionStatus: type: string enum: @@ -23441,6 +23418,11 @@ components: - COMPLIANCE_REJECTED - LIGHTNING_PAYMENT_FAILED - COUNTERPARTY_POST_TX_FAILED + - SWEEP_BELOW_MINIMUM + - SWEEP_ABOVE_MAXIMUM + - SWEEP_NO_ELIGIBLE_RAIL + - SWEEP_QUOTE_FAILED + - SWEEP_EXECUTION_FAILED description: | Reason for failure of an outgoing transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed @@ -23472,6 +23454,16 @@ components: | `COMPLIANCE_REJECTED` | The payout partner rejected the recipient on compliance grounds — sanctions, watchlist, or KYC/AML screening | | `LIGHTNING_PAYMENT_FAILED` | Deprecated — superseded by `QUOTE_EXECUTION_FAILED`. The Lightning settlement leg failed | | `COUNTERPARTY_POST_TX_FAILED` | Deprecated — coarse fallback retained for historical transactions when no granular payout reason is available | + | `SWEEP_BELOW_MINIMUM` | The payment settled into a rule-based account but was below the smallest amount the corridor to the rule's destination can carry | + | `SWEEP_ABOVE_MAXIMUM` | The payment settled into a rule-based account but was above the largest amount the corridor to the rule's destination can carry | + | `SWEEP_NO_ELIGIBLE_RAIL` | No payment rail available to the rule's destination could carry the sweep | + | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination | + | `SWEEP_EXECUTION_FAILED` | The sweep was priced but the resulting payment did not go through | + + The `SWEEP_*` reasons apply to a payment that settled into a rule-based + account and could not be swept onward. A rule-based account never holds a + balance, so the funds are moved to the account holder's own account in the + same currency. RailSelectionMode: type: string enum: @@ -23544,19 +23536,7 @@ components: description: Details about the rate and fees for the transaction. failureReason: $ref: '#/components/schemas/OutgoingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. Set only when `status` is `FAILED`. A rule-based account whose sweep was refused is not a failure of this transaction — see `sweepFailureReason`. - sweepFailureReason: - allOf: - - $ref: '#/components/schemas/SweepFailureReason' - description: |- - Why the onward sweep did not reach the rule's destination, when this payment settled into a rule-based account. - - Distinct from `failureReason`, and never set at the same time: the two describe different events and are told apart by `status`. This transaction **succeeded** — the deposit landed and `status` is `COMPLETED`; only the sweep out of the account was refused, and the funds are in `sweepCanonicalAccountId`. `failureReason` appears only when `status` is `FAILED`, meaning the payment itself did not credit. - - Omitted for any account that is not rule-based. - sweepCanonicalAccountId: - type: string - description: The account holder's own account in the same currency the funds were moved to when a sweep could not be completed — the customer's, or the platform's for a platform-owned rule-based account. A rule-based account never holds a balance. Present whenever `sweepFailureReason` is. + description: If the transaction failed, this field provides the reason for failure. A payment that settled into a rule-based account but could not be swept to the rule's destination fails here too, with one of the `SWEEP_*` reasons. paymentRail: anyOf: - $ref: '#/components/schemas/PaymentRail' diff --git a/openapi/components/schemas/transactions/IncomingTransaction.yaml b/openapi/components/schemas/transactions/IncomingTransaction.yaml index 750ed82ef..854a4308f 100644 --- a/openapi/components/schemas/transactions/IncomingTransaction.yaml +++ b/openapi/components/schemas/transactions/IncomingTransaction.yaml @@ -14,9 +14,10 @@ allOf: receivedAmount: $ref: ../common/CurrencyAmount.yaml description: >- - Amount received in the recipient's currency. Absent until the amount - is priced — a payment swept out of a rule-based account is created - when the deposit settles, before its quote locks a rate. + Amount received in the recipient's currency. Always present, except on + a rule-based account sweep that was never priced: that transaction is + created when the deposit settles, so a sweep failing before its quote + locks a rate has no received amount to report. fees: type: integer format: int64 @@ -32,31 +33,6 @@ allOf: $ref: ./IncomingTransactionFailureReason.yaml description: >- If the transaction failed, this field provides the reason for failure. - Set only when `status` is `FAILED`. A rule-based account whose sweep - was refused is not a failure of this transaction — see - `sweepFailureReason`. - sweepFailureReason: - allOf: - - $ref: ./SweepFailureReason.yaml - description: >- - Why the onward sweep did not reach the rule's destination, when this - payment settled into a rule-based account. - - - Distinct from `failureReason`, and never set at the same time: the two - describe different events and are told apart by `status`. This - transaction **succeeded** — the deposit landed and `status` is - `COMPLETED`; only the sweep out of the account was refused, and the - funds are in `sweepCanonicalAccountId`. `failureReason` appears only - when `status` is `FAILED`, meaning the payment itself did not credit. - - - Omitted for any account that is not rule-based. - sweepCanonicalAccountId: - type: string - description: >- - The account holder's own account in the same currency the funds were - moved to when a sweep could not be completed — the customer's, or the - platform's for a platform-owned rule-based account. A rule-based - account never holds a balance. Present whenever - `sweepFailureReason` is. + A payment that settled into a rule-based account but could not be + swept to the rule's destination fails here too, with one of the + `SWEEP_*` reasons. \ No newline at end of file diff --git a/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml b/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml index 5158d7f4e..2a770f447 100644 --- a/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml +++ b/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml @@ -10,6 +10,11 @@ enum: - QUOTE_EXECUTION_FAILED - COMPLIANCE_REJECTED - COLLECTION_FAILED + - SWEEP_BELOW_MINIMUM + - SWEEP_ABOVE_MAXIMUM + - SWEEP_NO_ELIGIBLE_RAIL + - SWEEP_QUOTE_FAILED + - SWEEP_EXECUTION_FAILED description: >- Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed @@ -20,3 +25,13 @@ description: >- information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. + + A payment that settled into a rule-based account but could not be swept to the + destination of the account's rule fails with one of the `SWEEP_*` reasons. A + rule-based account never holds a balance, so the funds are moved to the account + holder's own account in the same currency. SWEEP_BELOW_MINIMUM and + SWEEP_ABOVE_MAXIMUM mean the balance was outside the band the corridor to the + destination can carry. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available + to the destination could carry it. SWEEP_QUOTE_FAILED means Grid could not + price it, and SWEEP_EXECUTION_FAILED that it was priced but the resulting + payment did not go through. diff --git a/openapi/components/schemas/transactions/OutgoingTransaction.yaml b/openapi/components/schemas/transactions/OutgoingTransaction.yaml index b80b75fd9..7a2dfc732 100644 --- a/openapi/components/schemas/transactions/OutgoingTransaction.yaml +++ b/openapi/components/schemas/transactions/OutgoingTransaction.yaml @@ -72,34 +72,9 @@ allOf: $ref: ./OutgoingTransactionFailureReason.yaml description: >- If the transaction failed, this field provides the reason for failure. - Set only when `status` is `FAILED`. A rule-based account whose sweep - was refused is not a failure of this transaction — see - `sweepFailureReason`. - sweepFailureReason: - allOf: - - $ref: ./SweepFailureReason.yaml - description: >- - Why the onward sweep did not reach the rule's destination, when this - payment settled into a rule-based account. - - - Distinct from `failureReason`, and never set at the same time: the two - describe different events and are told apart by `status`. This - transaction **succeeded** — the deposit landed and `status` is - `COMPLETED`; only the sweep out of the account was refused, and the - funds are in `sweepCanonicalAccountId`. `failureReason` appears only - when `status` is `FAILED`, meaning the payment itself did not credit. - - - Omitted for any account that is not rule-based. - sweepCanonicalAccountId: - type: string - description: >- - The account holder's own account in the same currency the funds were - moved to when a sweep could not be completed — the customer's, or the - platform's for a platform-owned rule-based account. A rule-based - account never holds a balance. Present whenever - `sweepFailureReason` is. + A payment that settled into a rule-based account but could not be + swept to the rule's destination fails here too, with one of the + `SWEEP_*` reasons. paymentRail: anyOf: - $ref: ../common/PaymentRail.yaml diff --git a/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml b/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml index 38772c4dd..473384e15 100644 --- a/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml +++ b/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml @@ -12,6 +12,11 @@ enum: - COMPLIANCE_REJECTED - LIGHTNING_PAYMENT_FAILED - COUNTERPARTY_POST_TX_FAILED + - SWEEP_BELOW_MINIMUM + - SWEEP_ABOVE_MAXIMUM + - SWEEP_NO_ELIGIBLE_RAIL + - SWEEP_QUOTE_FAILED + - SWEEP_EXECUTION_FAILED description: | Reason for failure of an outgoing transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed @@ -43,3 +48,13 @@ description: | | `COMPLIANCE_REJECTED` | The payout partner rejected the recipient on compliance grounds — sanctions, watchlist, or KYC/AML screening | | `LIGHTNING_PAYMENT_FAILED` | Deprecated — superseded by `QUOTE_EXECUTION_FAILED`. The Lightning settlement leg failed | | `COUNTERPARTY_POST_TX_FAILED` | Deprecated — coarse fallback retained for historical transactions when no granular payout reason is available | + | `SWEEP_BELOW_MINIMUM` | The payment settled into a rule-based account but was below the smallest amount the corridor to the rule's destination can carry | + | `SWEEP_ABOVE_MAXIMUM` | The payment settled into a rule-based account but was above the largest amount the corridor to the rule's destination can carry | + | `SWEEP_NO_ELIGIBLE_RAIL` | No payment rail available to the rule's destination could carry the sweep | + | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination | + | `SWEEP_EXECUTION_FAILED` | The sweep was priced but the resulting payment did not go through | + + The `SWEEP_*` reasons apply to a payment that settled into a rule-based + account and could not be swept onward. A rule-based account never holds a + balance, so the funds are moved to the account holder's own account in the + same currency. diff --git a/openapi/components/schemas/transactions/SweepFailureReason.yaml b/openapi/components/schemas/transactions/SweepFailureReason.yaml deleted file mode 100644 index a7c9974b5..000000000 --- a/openapi/components/schemas/transactions/SweepFailureReason.yaml +++ /dev/null @@ -1,17 +0,0 @@ -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. | From 75b0de2b4f1b2880f15c180f6024e6dd89ad398b Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 3 Sep 2026 21:21:04 +0000 Subject: [PATCH 13/16] docs: use the reviewer's receivedAmount wording Shorter than the version it replaces and says the same thing. Co-Authored-By: shreyav --- mintlify/openapi.yaml | 2 +- openapi.yaml | 2 +- .../schemas/transactions/IncomingTransaction.yaml | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 40c68c344..44b2e565c 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -23323,7 +23323,7 @@ components: $ref: '#/components/schemas/TransactionSourceOneOf' receivedAmount: $ref: '#/components/schemas/CurrencyAmount' - description: 'Amount received in the recipient''s currency. Always present, except on a rule-based account sweep that was never priced: that transaction is created when the deposit settles, so a sweep failing before its quote locks a rate has no received amount to report.' + description: Amount received in the recipient's currency. This is only absent for rule-based account sweeps if the sweep couldn't be quoted. It's always present otherwise. fees: type: integer format: int64 diff --git a/openapi.yaml b/openapi.yaml index 40c68c344..44b2e565c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -23323,7 +23323,7 @@ components: $ref: '#/components/schemas/TransactionSourceOneOf' receivedAmount: $ref: '#/components/schemas/CurrencyAmount' - description: 'Amount received in the recipient''s currency. Always present, except on a rule-based account sweep that was never priced: that transaction is created when the deposit settles, so a sweep failing before its quote locks a rate has no received amount to report.' + description: Amount received in the recipient's currency. This is only absent for rule-based account sweeps if the sweep couldn't be quoted. It's always present otherwise. fees: type: integer format: int64 diff --git a/openapi/components/schemas/transactions/IncomingTransaction.yaml b/openapi/components/schemas/transactions/IncomingTransaction.yaml index 854a4308f..e93ece9ed 100644 --- a/openapi/components/schemas/transactions/IncomingTransaction.yaml +++ b/openapi/components/schemas/transactions/IncomingTransaction.yaml @@ -14,10 +14,9 @@ allOf: receivedAmount: $ref: ../common/CurrencyAmount.yaml description: >- - Amount received in the recipient's currency. Always present, except on - a rule-based account sweep that was never priced: that transaction is - created when the deposit settles, so a sweep failing before its quote - locks a rate has no received amount to report. + Amount received in the recipient's currency. This is only absent for + rule-based account sweeps if the sweep couldn't be quoted. It's always + present otherwise. fees: type: integer format: int64 From 32ce372cb7c78f20abcfda048fbbd7104e73fa86 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 3 Sep 2026 21:31:10 +0000 Subject: [PATCH 14/16] docs: trim the sweep failure reasons to three MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 20 +++++++------------ openapi.yaml | 20 +++++++------------ .../IncomingTransactionFailureReason.yaml | 17 ++++++++-------- .../OutgoingTransactionFailureReason.yaml | 10 +++------- .../agents/agents_me_internal-accounts.yaml | 6 ++++-- .../platform/platform_internal_accounts.yaml | 6 ++++-- 6 files changed, 33 insertions(+), 46 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 44b2e565c..53cf6e6fb 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2312,7 +2312,7 @@ paths: type: string - name: type in: query - description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts. + description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts, or `RULE_BASED` for the additional account numbers issued with a sweep rule. required: false schema: $ref: '#/components/schemas/InternalAccountType' @@ -8304,7 +8304,7 @@ paths: type: string - name: type in: query - description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for the customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for platform-managed holding accounts. + description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for the customer, `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for platform-managed holding accounts, or `RULE_BASED` for the additional account numbers issued with a sweep rule. required: false schema: $ref: '#/components/schemas/InternalAccountType' @@ -23299,14 +23299,12 @@ components: - QUOTE_EXECUTION_FAILED - COMPLIANCE_REJECTED - COLLECTION_FAILED - - SWEEP_BELOW_MINIMUM - - SWEEP_ABOVE_MAXIMUM + - SWEEP_AMOUNT_OUT_OF_RANGE - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED - - SWEEP_EXECUTION_FAILED description: |- Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. - A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_BELOW_MINIMUM and SWEEP_ABOVE_MAXIMUM mean the balance was outside the band the corridor to the destination can carry. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available to the destination could carry it. SWEEP_QUOTE_FAILED means Grid could not price it, and SWEEP_EXECUTION_FAILED that it was priced but the resulting payment did not go through. + A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_AMOUNT_OUT_OF_RANGE means the amount was outside the band the corridor to the destination can carry — the rule's minimumAmount and maximumAmount give the current band. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available to the destination could carry it, and SWEEP_QUOTE_FAILED that Grid could not price it. A sweep that was priced and then failed on the way to settlement reports QUOTE_EXECUTION_FAILED, the same as any other payment. IncomingTransaction: title: Incoming Transaction allOf: @@ -23418,11 +23416,9 @@ components: - COMPLIANCE_REJECTED - LIGHTNING_PAYMENT_FAILED - COUNTERPARTY_POST_TX_FAILED - - SWEEP_BELOW_MINIMUM - - SWEEP_ABOVE_MAXIMUM + - SWEEP_AMOUNT_OUT_OF_RANGE - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED - - SWEEP_EXECUTION_FAILED description: | Reason for failure of an outgoing transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed @@ -23454,11 +23450,9 @@ components: | `COMPLIANCE_REJECTED` | The payout partner rejected the recipient on compliance grounds — sanctions, watchlist, or KYC/AML screening | | `LIGHTNING_PAYMENT_FAILED` | Deprecated — superseded by `QUOTE_EXECUTION_FAILED`. The Lightning settlement leg failed | | `COUNTERPARTY_POST_TX_FAILED` | Deprecated — coarse fallback retained for historical transactions when no granular payout reason is available | - | `SWEEP_BELOW_MINIMUM` | The payment settled into a rule-based account but was below the smallest amount the corridor to the rule's destination can carry | - | `SWEEP_ABOVE_MAXIMUM` | The payment settled into a rule-based account but was above the largest amount the corridor to the rule's destination can carry | + | `SWEEP_AMOUNT_OUT_OF_RANGE` | The amount was outside the range the corridor to the rule's destination can carry — below its minimum or above its maximum. The rule's `minimumAmount` and `maximumAmount` give the current band | | `SWEEP_NO_ELIGIBLE_RAIL` | No payment rail available to the rule's destination could carry the sweep | - | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination | - | `SWEEP_EXECUTION_FAILED` | The sweep was priced but the resulting payment did not go through | + | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination. A sweep that was priced and then failed on the way to settlement reports `QUOTE_EXECUTION_FAILED`, the same as any other payout | The `SWEEP_*` reasons apply to a payment that settled into a rule-based account and could not be swept onward. A rule-based account never holds a diff --git a/openapi.yaml b/openapi.yaml index 44b2e565c..53cf6e6fb 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2312,7 +2312,7 @@ paths: type: string - name: type in: query - description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts. + description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts, or `RULE_BASED` for the additional account numbers issued with a sweep rule. required: false schema: $ref: '#/components/schemas/InternalAccountType' @@ -8304,7 +8304,7 @@ paths: type: string - name: type in: query - description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for the customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for platform-managed holding accounts. + description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for the customer, `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for platform-managed holding accounts, or `RULE_BASED` for the additional account numbers issued with a sweep rule. required: false schema: $ref: '#/components/schemas/InternalAccountType' @@ -23299,14 +23299,12 @@ components: - QUOTE_EXECUTION_FAILED - COMPLIANCE_REJECTED - COLLECTION_FAILED - - SWEEP_BELOW_MINIMUM - - SWEEP_ABOVE_MAXIMUM + - SWEEP_AMOUNT_OUT_OF_RANGE - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED - - SWEEP_EXECUTION_FAILED description: |- Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. - A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_BELOW_MINIMUM and SWEEP_ABOVE_MAXIMUM mean the balance was outside the band the corridor to the destination can carry. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available to the destination could carry it. SWEEP_QUOTE_FAILED means Grid could not price it, and SWEEP_EXECUTION_FAILED that it was priced but the resulting payment did not go through. + A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_AMOUNT_OUT_OF_RANGE means the amount was outside the band the corridor to the destination can carry — the rule's minimumAmount and maximumAmount give the current band. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available to the destination could carry it, and SWEEP_QUOTE_FAILED that Grid could not price it. A sweep that was priced and then failed on the way to settlement reports QUOTE_EXECUTION_FAILED, the same as any other payment. IncomingTransaction: title: Incoming Transaction allOf: @@ -23418,11 +23416,9 @@ components: - COMPLIANCE_REJECTED - LIGHTNING_PAYMENT_FAILED - COUNTERPARTY_POST_TX_FAILED - - SWEEP_BELOW_MINIMUM - - SWEEP_ABOVE_MAXIMUM + - SWEEP_AMOUNT_OUT_OF_RANGE - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED - - SWEEP_EXECUTION_FAILED description: | Reason for failure of an outgoing transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed @@ -23454,11 +23450,9 @@ components: | `COMPLIANCE_REJECTED` | The payout partner rejected the recipient on compliance grounds — sanctions, watchlist, or KYC/AML screening | | `LIGHTNING_PAYMENT_FAILED` | Deprecated — superseded by `QUOTE_EXECUTION_FAILED`. The Lightning settlement leg failed | | `COUNTERPARTY_POST_TX_FAILED` | Deprecated — coarse fallback retained for historical transactions when no granular payout reason is available | - | `SWEEP_BELOW_MINIMUM` | The payment settled into a rule-based account but was below the smallest amount the corridor to the rule's destination can carry | - | `SWEEP_ABOVE_MAXIMUM` | The payment settled into a rule-based account but was above the largest amount the corridor to the rule's destination can carry | + | `SWEEP_AMOUNT_OUT_OF_RANGE` | The amount was outside the range the corridor to the rule's destination can carry — below its minimum or above its maximum. The rule's `minimumAmount` and `maximumAmount` give the current band | | `SWEEP_NO_ELIGIBLE_RAIL` | No payment rail available to the rule's destination could carry the sweep | - | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination | - | `SWEEP_EXECUTION_FAILED` | The sweep was priced but the resulting payment did not go through | + | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination. A sweep that was priced and then failed on the way to settlement reports `QUOTE_EXECUTION_FAILED`, the same as any other payout | The `SWEEP_*` reasons apply to a payment that settled into a rule-based account and could not be swept onward. A rule-based account never holds a diff --git a/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml b/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml index 2a770f447..5c895f6f9 100644 --- a/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml +++ b/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml @@ -10,11 +10,9 @@ enum: - QUOTE_EXECUTION_FAILED - COMPLIANCE_REJECTED - COLLECTION_FAILED - - SWEEP_BELOW_MINIMUM - - SWEEP_ABOVE_MAXIMUM + - SWEEP_AMOUNT_OUT_OF_RANGE - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED - - SWEEP_EXECUTION_FAILED description: >- Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed @@ -29,9 +27,10 @@ description: >- A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account - holder's own account in the same currency. SWEEP_BELOW_MINIMUM and - SWEEP_ABOVE_MAXIMUM mean the balance was outside the band the corridor to the - destination can carry. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available - to the destination could carry it. SWEEP_QUOTE_FAILED means Grid could not - price it, and SWEEP_EXECUTION_FAILED that it was priced but the resulting - payment did not go through. + holder's own account in the same currency. SWEEP_AMOUNT_OUT_OF_RANGE means the + amount was outside the band the corridor to the destination can carry — the + rule's minimumAmount and maximumAmount give the current band. + SWEEP_NO_ELIGIBLE_RAIL means no payment rail available + to the destination could carry it, and SWEEP_QUOTE_FAILED that Grid could not + price it. A sweep that was priced and then failed on the way to settlement + reports QUOTE_EXECUTION_FAILED, the same as any other payment. diff --git a/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml b/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml index 473384e15..c8f88280d 100644 --- a/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml +++ b/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml @@ -12,11 +12,9 @@ enum: - COMPLIANCE_REJECTED - LIGHTNING_PAYMENT_FAILED - COUNTERPARTY_POST_TX_FAILED - - SWEEP_BELOW_MINIMUM - - SWEEP_ABOVE_MAXIMUM + - SWEEP_AMOUNT_OUT_OF_RANGE - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED - - SWEEP_EXECUTION_FAILED description: | Reason for failure of an outgoing transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed @@ -48,11 +46,9 @@ description: | | `COMPLIANCE_REJECTED` | The payout partner rejected the recipient on compliance grounds — sanctions, watchlist, or KYC/AML screening | | `LIGHTNING_PAYMENT_FAILED` | Deprecated — superseded by `QUOTE_EXECUTION_FAILED`. The Lightning settlement leg failed | | `COUNTERPARTY_POST_TX_FAILED` | Deprecated — coarse fallback retained for historical transactions when no granular payout reason is available | - | `SWEEP_BELOW_MINIMUM` | The payment settled into a rule-based account but was below the smallest amount the corridor to the rule's destination can carry | - | `SWEEP_ABOVE_MAXIMUM` | The payment settled into a rule-based account but was above the largest amount the corridor to the rule's destination can carry | + | `SWEEP_AMOUNT_OUT_OF_RANGE` | The amount was outside the range the corridor to the rule's destination can carry — below its minimum or above its maximum. The rule's `minimumAmount` and `maximumAmount` give the current band | | `SWEEP_NO_ELIGIBLE_RAIL` | No payment rail available to the rule's destination could carry the sweep | - | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination | - | `SWEEP_EXECUTION_FAILED` | The sweep was priced but the resulting payment did not go through | + | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination. A sweep that was priced and then failed on the way to settlement reports `QUOTE_EXECUTION_FAILED`, the same as any other payout | The `SWEEP_*` reasons apply to a payment that settled into a rule-based account and could not be swept onward. A rule-based account never holds a diff --git a/openapi/paths/agents/agents_me_internal-accounts.yaml b/openapi/paths/agents/agents_me_internal-accounts.yaml index 7ce79b09c..b2e478ca9 100644 --- a/openapi/paths/agents/agents_me_internal-accounts.yaml +++ b/openapi/paths/agents/agents_me_internal-accounts.yaml @@ -20,8 +20,10 @@ get: in: query description: >- Filter by internal account type. Use `EMBEDDED_WALLET` to find the - self-custodial wallet provisioned for the customer, or `INTERNAL_FIAT` / - `INTERNAL_CRYPTO` for platform-managed holding accounts. + self-custodial wallet provisioned for the customer, `INTERNAL_FIAT` / + `INTERNAL_CRYPTO` for platform-managed holding accounts, or + `RULE_BASED` for the additional account numbers issued with a sweep + rule. required: false schema: $ref: ../../components/schemas/customers/InternalAccountType.yaml diff --git a/openapi/paths/platform/platform_internal_accounts.yaml b/openapi/paths/platform/platform_internal_accounts.yaml index 5c7b18fa5..c7b418680 100644 --- a/openapi/paths/platform/platform_internal_accounts.yaml +++ b/openapi/paths/platform/platform_internal_accounts.yaml @@ -20,8 +20,10 @@ get: in: query description: >- Filter by internal account type. Use `EMBEDDED_WALLET` to find the - self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / - `INTERNAL_CRYPTO` for the platform-managed holding accounts. + self-custodial wallet provisioned for a customer, `INTERNAL_FIAT` / + `INTERNAL_CRYPTO` for the platform-managed holding accounts, or + `RULE_BASED` for the additional account numbers issued with a sweep + rule. required: false schema: $ref: ../../components/schemas/customers/InternalAccountType.yaml From a6c059132e33b64652a12b73a9203a25e8d206ac Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 3 Sep 2026 21:33:06 +0000 Subject: [PATCH 15/16] docs: fold the sweep rail failure into ACCOUNT_CANNOT_RECEIVE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- mintlify/openapi.yaml | 7 ++----- openapi.yaml | 7 ++----- .../transactions/IncomingTransactionFailureReason.yaml | 10 ++++------ .../transactions/OutgoingTransactionFailureReason.yaml | 4 +--- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 53cf6e6fb..ec571effd 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -23300,11 +23300,10 @@ components: - COMPLIANCE_REJECTED - COLLECTION_FAILED - SWEEP_AMOUNT_OUT_OF_RANGE - - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED description: |- Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. - A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_AMOUNT_OUT_OF_RANGE means the amount was outside the band the corridor to the destination can carry — the rule's minimumAmount and maximumAmount give the current band. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available to the destination could carry it, and SWEEP_QUOTE_FAILED that Grid could not price it. A sweep that was priced and then failed on the way to settlement reports QUOTE_EXECUTION_FAILED, the same as any other payment. + A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_AMOUNT_OUT_OF_RANGE means the amount was outside the band the corridor to the destination can carry — the rule's minimumAmount and maximumAmount give the current band — and SWEEP_QUOTE_FAILED that Grid could not price it. A sweep that was priced and then failed on the way to settlement reports QUOTE_EXECUTION_FAILED, the same as any other payment. IncomingTransaction: title: Incoming Transaction allOf: @@ -23417,7 +23416,6 @@ components: - LIGHTNING_PAYMENT_FAILED - COUNTERPARTY_POST_TX_FAILED - SWEEP_AMOUNT_OUT_OF_RANGE - - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED description: | Reason for failure of an outgoing transaction. This is used to provide more @@ -23445,13 +23443,12 @@ components: | `SCA_NOT_COMPLETED` | The customer did not complete the Strong Customer Authentication challenge before it expired | | `PAYOUT_RETURNED` | The receiving bank accepted the payout and then returned or reversed it | | `LIMIT_EXCEEDED` | The payout exceeds a recipient-, account-, or corridor-level limit at the partner | - | `ACCOUNT_CANNOT_RECEIVE` | The account exists but can't accept this payment — dormant, frozen, restricted, or the currency isn't supported for that account | + | `ACCOUNT_CANNOT_RECEIVE` | The account exists but can't accept this payment — dormant, frozen, restricted, or the currency or payment rail isn't supported for that account. A sweep whose rule names a rail the destination cannot accept reports this | | `ACCOUNT_INVALID` | The recipient account couldn't be found or the details are wrong — bad IBAN/account number, beneficiary not found, or account closed | | `COMPLIANCE_REJECTED` | The payout partner rejected the recipient on compliance grounds — sanctions, watchlist, or KYC/AML screening | | `LIGHTNING_PAYMENT_FAILED` | Deprecated — superseded by `QUOTE_EXECUTION_FAILED`. The Lightning settlement leg failed | | `COUNTERPARTY_POST_TX_FAILED` | Deprecated — coarse fallback retained for historical transactions when no granular payout reason is available | | `SWEEP_AMOUNT_OUT_OF_RANGE` | The amount was outside the range the corridor to the rule's destination can carry — below its minimum or above its maximum. The rule's `minimumAmount` and `maximumAmount` give the current band | - | `SWEEP_NO_ELIGIBLE_RAIL` | No payment rail available to the rule's destination could carry the sweep | | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination. A sweep that was priced and then failed on the way to settlement reports `QUOTE_EXECUTION_FAILED`, the same as any other payout | The `SWEEP_*` reasons apply to a payment that settled into a rule-based diff --git a/openapi.yaml b/openapi.yaml index 53cf6e6fb..ec571effd 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -23300,11 +23300,10 @@ components: - COMPLIANCE_REJECTED - COLLECTION_FAILED - SWEEP_AMOUNT_OUT_OF_RANGE - - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED description: |- Reason for failure of an incoming transaction. This is used to provide more context on why a transaction failed. If the transaction is not in a failed state, this field is omitted. COMPLIANCE_REJECTED means compliance blocked the incoming funds. COLLECTION_FAILED means an inbound bank-rail collection deposit (e.g. a SWIFT wire) failed after arrival and did not credit — most commonly a compliance-screening rejection or an unresolved request for information, on either the collection provider's side or Grid's. Failed collections are returned to the sender per the collection provider's process. - A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_AMOUNT_OUT_OF_RANGE means the amount was outside the band the corridor to the destination can carry — the rule's minimumAmount and maximumAmount give the current band. SWEEP_NO_ELIGIBLE_RAIL means no payment rail available to the destination could carry it, and SWEEP_QUOTE_FAILED that Grid could not price it. A sweep that was priced and then failed on the way to settlement reports QUOTE_EXECUTION_FAILED, the same as any other payment. + A payment that settled into a rule-based account but could not be swept to the destination of the account's rule fails with one of the `SWEEP_*` reasons. A rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_AMOUNT_OUT_OF_RANGE means the amount was outside the band the corridor to the destination can carry — the rule's minimumAmount and maximumAmount give the current band — and SWEEP_QUOTE_FAILED that Grid could not price it. A sweep that was priced and then failed on the way to settlement reports QUOTE_EXECUTION_FAILED, the same as any other payment. IncomingTransaction: title: Incoming Transaction allOf: @@ -23417,7 +23416,6 @@ components: - LIGHTNING_PAYMENT_FAILED - COUNTERPARTY_POST_TX_FAILED - SWEEP_AMOUNT_OUT_OF_RANGE - - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED description: | Reason for failure of an outgoing transaction. This is used to provide more @@ -23445,13 +23443,12 @@ components: | `SCA_NOT_COMPLETED` | The customer did not complete the Strong Customer Authentication challenge before it expired | | `PAYOUT_RETURNED` | The receiving bank accepted the payout and then returned or reversed it | | `LIMIT_EXCEEDED` | The payout exceeds a recipient-, account-, or corridor-level limit at the partner | - | `ACCOUNT_CANNOT_RECEIVE` | The account exists but can't accept this payment — dormant, frozen, restricted, or the currency isn't supported for that account | + | `ACCOUNT_CANNOT_RECEIVE` | The account exists but can't accept this payment — dormant, frozen, restricted, or the currency or payment rail isn't supported for that account. A sweep whose rule names a rail the destination cannot accept reports this | | `ACCOUNT_INVALID` | The recipient account couldn't be found or the details are wrong — bad IBAN/account number, beneficiary not found, or account closed | | `COMPLIANCE_REJECTED` | The payout partner rejected the recipient on compliance grounds — sanctions, watchlist, or KYC/AML screening | | `LIGHTNING_PAYMENT_FAILED` | Deprecated — superseded by `QUOTE_EXECUTION_FAILED`. The Lightning settlement leg failed | | `COUNTERPARTY_POST_TX_FAILED` | Deprecated — coarse fallback retained for historical transactions when no granular payout reason is available | | `SWEEP_AMOUNT_OUT_OF_RANGE` | The amount was outside the range the corridor to the rule's destination can carry — below its minimum or above its maximum. The rule's `minimumAmount` and `maximumAmount` give the current band | - | `SWEEP_NO_ELIGIBLE_RAIL` | No payment rail available to the rule's destination could carry the sweep | | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination. A sweep that was priced and then failed on the way to settlement reports `QUOTE_EXECUTION_FAILED`, the same as any other payout | The `SWEEP_*` reasons apply to a payment that settled into a rule-based diff --git a/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml b/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml index 5c895f6f9..460d38c1c 100644 --- a/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml +++ b/openapi/components/schemas/transactions/IncomingTransactionFailureReason.yaml @@ -11,7 +11,6 @@ enum: - COMPLIANCE_REJECTED - COLLECTION_FAILED - SWEEP_AMOUNT_OUT_OF_RANGE - - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED description: >- Reason for failure of an incoming transaction. This is used to provide more @@ -29,8 +28,7 @@ description: >- rule-based account never holds a balance, so the funds are moved to the account holder's own account in the same currency. SWEEP_AMOUNT_OUT_OF_RANGE means the amount was outside the band the corridor to the destination can carry — the - rule's minimumAmount and maximumAmount give the current band. - SWEEP_NO_ELIGIBLE_RAIL means no payment rail available - to the destination could carry it, and SWEEP_QUOTE_FAILED that Grid could not - price it. A sweep that was priced and then failed on the way to settlement - reports QUOTE_EXECUTION_FAILED, the same as any other payment. + rule's minimumAmount and maximumAmount give the current band — and + SWEEP_QUOTE_FAILED that Grid could not price it. A sweep that was priced and + then failed on the way to settlement reports QUOTE_EXECUTION_FAILED, the same + as any other payment. diff --git a/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml b/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml index c8f88280d..6011f5d8e 100644 --- a/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml +++ b/openapi/components/schemas/transactions/OutgoingTransactionFailureReason.yaml @@ -13,7 +13,6 @@ enum: - LIGHTNING_PAYMENT_FAILED - COUNTERPARTY_POST_TX_FAILED - SWEEP_AMOUNT_OUT_OF_RANGE - - SWEEP_NO_ELIGIBLE_RAIL - SWEEP_QUOTE_FAILED description: | Reason for failure of an outgoing transaction. This is used to provide more @@ -41,13 +40,12 @@ description: | | `SCA_NOT_COMPLETED` | The customer did not complete the Strong Customer Authentication challenge before it expired | | `PAYOUT_RETURNED` | The receiving bank accepted the payout and then returned or reversed it | | `LIMIT_EXCEEDED` | The payout exceeds a recipient-, account-, or corridor-level limit at the partner | - | `ACCOUNT_CANNOT_RECEIVE` | The account exists but can't accept this payment — dormant, frozen, restricted, or the currency isn't supported for that account | + | `ACCOUNT_CANNOT_RECEIVE` | The account exists but can't accept this payment — dormant, frozen, restricted, or the currency or payment rail isn't supported for that account. A sweep whose rule names a rail the destination cannot accept reports this | | `ACCOUNT_INVALID` | The recipient account couldn't be found or the details are wrong — bad IBAN/account number, beneficiary not found, or account closed | | `COMPLIANCE_REJECTED` | The payout partner rejected the recipient on compliance grounds — sanctions, watchlist, or KYC/AML screening | | `LIGHTNING_PAYMENT_FAILED` | Deprecated — superseded by `QUOTE_EXECUTION_FAILED`. The Lightning settlement leg failed | | `COUNTERPARTY_POST_TX_FAILED` | Deprecated — coarse fallback retained for historical transactions when no granular payout reason is available | | `SWEEP_AMOUNT_OUT_OF_RANGE` | The amount was outside the range the corridor to the rule's destination can carry — below its minimum or above its maximum. The rule's `minimumAmount` and `maximumAmount` give the current band | - | `SWEEP_NO_ELIGIBLE_RAIL` | No payment rail available to the rule's destination could carry the sweep | | `SWEEP_QUOTE_FAILED` | Grid could not price the sweep to the rule's destination. A sweep that was priced and then failed on the way to settlement reports `QUOTE_EXECUTION_FAILED`, the same as any other payout | The `SWEEP_*` reasons apply to a payment that settled into a rule-based From 2eeefcf52a214452b6bccc13ade53fa3b66abe1f Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 3 Sep 2026 21:47:37 +0000 Subject: [PATCH 16/16] docs: drop the failureReason sweep note 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 --- mintlify/openapi.yaml | 4 ++-- openapi.yaml | 4 ++-- .../schemas/transactions/IncomingTransaction.yaml | 6 +----- .../schemas/transactions/OutgoingTransaction.yaml | 6 +----- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index ec571effd..43321c8af 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -23332,7 +23332,7 @@ components: description: Included for all transactions except those with "CREATED" status failureReason: $ref: '#/components/schemas/IncomingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. A payment that settled into a rule-based account but could not be swept to the rule's destination fails here too, with one of the `SWEEP_*` reasons. + description: If the transaction failed, this field provides the reason for failure. OutgoingTransactionStatus: type: string enum: @@ -23527,7 +23527,7 @@ components: description: Details about the rate and fees for the transaction. failureReason: $ref: '#/components/schemas/OutgoingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. A payment that settled into a rule-based account but could not be swept to the rule's destination fails here too, with one of the `SWEEP_*` reasons. + description: If the transaction failed, this field provides the reason for failure. paymentRail: anyOf: - $ref: '#/components/schemas/PaymentRail' diff --git a/openapi.yaml b/openapi.yaml index ec571effd..43321c8af 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -23332,7 +23332,7 @@ components: description: Included for all transactions except those with "CREATED" status failureReason: $ref: '#/components/schemas/IncomingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. A payment that settled into a rule-based account but could not be swept to the rule's destination fails here too, with one of the `SWEEP_*` reasons. + description: If the transaction failed, this field provides the reason for failure. OutgoingTransactionStatus: type: string enum: @@ -23527,7 +23527,7 @@ components: description: Details about the rate and fees for the transaction. failureReason: $ref: '#/components/schemas/OutgoingTransactionFailureReason' - description: If the transaction failed, this field provides the reason for failure. A payment that settled into a rule-based account but could not be swept to the rule's destination fails here too, with one of the `SWEEP_*` reasons. + description: If the transaction failed, this field provides the reason for failure. paymentRail: anyOf: - $ref: '#/components/schemas/PaymentRail' diff --git a/openapi/components/schemas/transactions/IncomingTransaction.yaml b/openapi/components/schemas/transactions/IncomingTransaction.yaml index e93ece9ed..a75438e0e 100644 --- a/openapi/components/schemas/transactions/IncomingTransaction.yaml +++ b/openapi/components/schemas/transactions/IncomingTransaction.yaml @@ -30,8 +30,4 @@ allOf: description: Included for all transactions except those with "CREATED" status failureReason: $ref: ./IncomingTransactionFailureReason.yaml - description: >- - If the transaction failed, this field provides the reason for failure. - A payment that settled into a rule-based account but could not be - swept to the rule's destination fails here too, with one of the - `SWEEP_*` reasons. \ No newline at end of file + description: If the transaction failed, this field provides the reason for failure. diff --git a/openapi/components/schemas/transactions/OutgoingTransaction.yaml b/openapi/components/schemas/transactions/OutgoingTransaction.yaml index 7a2dfc732..19919cc2e 100644 --- a/openapi/components/schemas/transactions/OutgoingTransaction.yaml +++ b/openapi/components/schemas/transactions/OutgoingTransaction.yaml @@ -70,11 +70,7 @@ allOf: description: Details about the rate and fees for the transaction. failureReason: $ref: ./OutgoingTransactionFailureReason.yaml - description: >- - If the transaction failed, this field provides the reason for failure. - A payment that settled into a rule-based account but could not be - swept to the rule's destination fails here too, with one of the - `SWEEP_*` reasons. + description: If the transaction failed, this field provides the reason for failure. paymentRail: anyOf: - $ref: ../common/PaymentRail.yaml