diff --git a/.redocly.lint-ignore.yaml b/.redocly.lint-ignore.yaml index 9652a98a5..455cde07d 100644 --- a/.redocly.lint-ignore.yaml +++ b/.redocly.lint-ignore.yaml @@ -6,6 +6,14 @@ openapi.yaml: no-required-schema-properties-undefined: - '#/components/schemas/QuoteSource/required/0' - '#/components/schemas/Quote/properties/destination/required/0' + # anyOf proof-requirement branches reference properties defined at the + # parent schema level (valid JSON Schema; the rule is overly strict here). + - '#/components/schemas/ScaLoginCompleteRequest/anyOf/0/required/0' + - '#/components/schemas/ScaLoginCompleteRequest/anyOf/1/required/0' + - '#/components/schemas/ScaLoginCompleteRequest/anyOf/1/required/1' + - '#/components/schemas/BeneficiaryTrustConfirmRequest/anyOf/0/required/0' + - '#/components/schemas/BeneficiaryTrustConfirmRequest/anyOf/1/required/0' + - '#/components/schemas/BeneficiaryTrustConfirmRequest/anyOf/1/required/1' no-invalid-media-type-examples: - >- #/paths/~1customers~1external-accounts/post/requestBody/content/application~1json/schema diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 1e57d7e52..40e821c1a 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -1127,180 +1127,97 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/internal-accounts: + /sca/factors: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer whose factors are listed or enrolled. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 get: - summary: List Customer internal accounts + summary: List enrolled SCA factors description: | - Retrieve a list of internal accounts with optional filtering parameters. Returns all - internal accounts that match the specified filters. If no filters are provided, returns all internal accounts - (paginated). + List the Strong Customer Authentication factors the customer has enrolled. - Internal accounts are created automatically when a customer is created based on the platform configuration. - operationId: listCustomerInternalAccounts + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: listScaFactors tags: - - Internal Accounts + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: currency - in: query - description: Filter by currency code - required: false - schema: - type: string - - name: customerId - in: query - description: Filter by internal accounts associated with a specific customer - required: false - schema: - 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. - required: false - schema: - $ref: '#/components/schemas/InternalAccountType' - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string responses: '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/InternalAccountListResponse' - '400': - description: Bad request - Invalid parameters + description: The customer's enrolled SCA factors. content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/ScaFactorList' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /platform/internal-accounts: - get: - summary: List platform internal accounts - description: | - Retrieve a list of all internal accounts that belong to the platform, as opposed to an individual customer. - - These accounts are created automatically when the platform is configured for each supported currency. They can be used for things like distributing bitcoin rewards to customers, or for other platform-wide purposes. - operationId: listPlatformInternalAccounts - tags: - - Internal Accounts - security: - - BasicAuth: [] - parameters: - - name: currency - in: query - description: Filter by currency code - required: false - schema: - 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. - required: false - schema: - $ref: '#/components/schemas/InternalAccountType' - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/PlatformInternalAccountListResponse' - '400': - description: Bad request - Invalid parameters + '404': + description: Customer not found content: application/json: schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. content: application/json: schema: - $ref: '#/components/schemas/Error401' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/external-accounts: - get: - summary: List Customer external accounts + post: + summary: Start SCA factor enrollment description: | - Retrieve a list of external accounts with optional filtering parameters. Returns all - external accounts that match the specified filters. If no filters are provided, returns all external accounts - (paginated). + Begin enrolling an SCA factor for the customer. Enrollment covers the + explicit, opt-in factors a customer chooses to add — the request body's + `type` selects `TOTP` or `PASSKEY`. Returns the factor-specific material + needed to finish via `POST /sca/factors/confirm`. - External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform. - operationId: listCustomerExternalAccounts + `SMS_OTP` is implicit and is not enrolled through this endpoint. Every + customer in an SCA-regulated region has a verified phone number from + customer creation (via the Contact Verification flows — + `POST /customers/{customerId}/verify-phone` and `.../verify-phone/confirm`), + so SMS is always available as a factor with no extra setup and appears + among the customer's enrolled factors in `GET /sca/factors`. + + A customer may have **only one passkey**. Starting a passkey enrollment when + one is already enrolled returns `409` (`PASSKEY_ALREADY_ENROLLED`) — delete it + via `DELETE /sca/factors/{credentialId}` first. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startScaFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: currency - in: query - description: Filter by currency code - required: false - schema: - type: string - - name: customerId - in: query - description: Filter by external accounts associated with a specific customer - required: false - schema: - type: string - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaFactorEnrollRequestOneOf' responses: '200': - description: Successful operation + description: Enrollment started; the factor-specific completion material is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' + $ref: '#/components/schemas/ScaFactorEnrollStartOneOf' '400': - description: Bad request - Invalid parameters + description: Invalid request content: application/json: schema: @@ -1311,18 +1228,52 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer (`CONFLICT`), or a passkey enrollment was requested while one is already enrolled (`PASSKEY_ALREADY_ENROLLED`) — only one passkey per customer is supported. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /sca/factors/confirm: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer confirming a factor enrollment. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Add a new external account - description: Register a new external bank account for a customer. - operationId: createCustomerExternalAccount + summary: Confirm SCA factor enrollment + description: | + Finalize the factor enrollment started by `POST /sca/factors`. The request + body is discriminated by `type`: for `TOTP`, submit the shared `secret` from + the start call plus the first `code`; for `PASSKEY`, submit the WebAuthn + `credential` the device produced plus the `origin` it was produced against. + The threaded secret/credential binds the confirmation to its enrollment, so + no separate id is needed. + + A TOTP confirmation returns one-time recovery codes (shown once); a passkey + confirmation returns the enrolled factor. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the TOTP code is always `123456`. + operationId: confirmScaFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1330,48 +1281,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountCreateRequest' - examples: - usBankAccount: - summary: Create external US bank account - value: - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - currency: USD - accountInfo: - accountType: USD_ACCOUNT - accountNumber: '12345678901' - routingNumber: '123456789' - bankAccountType: CHECKING - bankName: Chase Bank - platformAccountId: ext_acc_123456 - beneficiary: - beneficiaryType: INDIVIDUAL - fullName: John Doe - birthDate: '1990-01-15' - nationality: US - address: - line1: 123 Main Street - city: San Francisco - state: CA - postalCode: '94105' - country: US - sparkWallet: - summary: Create external Spark wallet - value: - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - currency: BTC - accountInfo: - accountType: SPARK_WALLET - address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu + $ref: '#/components/schemas/ScaFactorConfirmRequestOneOf' responses: - '201': - description: External account created successfully + '200': + description: Factor enrolled; the factor-specific result is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/ScaFactorConfirmResponseOneOf' '400': - description: Bad request + description: Invalid or incorrect confirmation proof content: application/json: schema: @@ -1382,8 +1301,14 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '409': - description: Conflict - External account already exists + description: SCA is not required for this customer. content: application/json: schema: @@ -1394,29 +1319,36 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/external-accounts/{externalAccountId}: + /sca/factors/{credentialId}: parameters: - - name: externalAccountId + - name: customerId + in: query + description: The unique identifier of the customer whose factor is being deleted. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: credentialId in: path - description: System-generated unique external account identifier + description: The credential id of the enrolled factor to delete (from the factor's `credentialId`). required: true schema: type: string - get: - summary: Get customer external account by ID - description: Retrieve a customer external account by its system-generated ID - operationId: getCustomerExternalAccountById + delete: + summary: Delete an enrolled SCA factor + description: | + Delete an enrolled SCA factor by its credential id. Today only `PASSKEY` + factors carry a `credentialId` and are deletable this way. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: deleteScaFactor tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ExternalAccount' + '204': + description: Factor deleted; no content is returned. '401': description: Unauthorized content: @@ -1424,89 +1356,64 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer or factor not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '500': - description: Internal service error + '409': + description: SCA is not required for this customer. content: application/json: schema: - $ref: '#/components/schemas/Error500' - delete: - summary: Delete customer external account by ID - description: Delete a customer external account by its system-generated ID - operationId: deleteCustomerExternalAccountById - tags: - - External Accounts - security: - - BasicAuth: [] - responses: - '204': - description: External account deleted successfully - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: External account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts: - get: - summary: List platform external accounts + /sca/login/start: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer starting an SCA login. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Start an SCA login description: | - Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer. + Begin an SCA login for the customer with the chosen factor, opening the + end-user SCA session (an exemption gating read / account access beyond the + per-transaction window). Returns factor-specific material: `SMS_OTP` + dispatches a code and returns a `challengeId` + `expiresAt`; `TOTP` returns + only the factor (the customer reads the code from their app); `PASSKEY` + returns WebAuthn `passkeyOptions`. Complete with + `POST /sca/login/complete`. - These accounts are used for platform-wide operations such as receiving funds from external sources or managing platform-level payment destinations. - operationId: listPlatformExternalAccounts + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startScaLogin tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: currency - in: query - description: Filter by currency code - required: false - schema: - type: string - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaLoginStartRequest' responses: '200': - description: Successful operation + description: SCA login started; factor-specific material is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' + $ref: '#/components/schemas/ScaLoginStart' '400': - description: Bad request - Invalid parameters + description: Invalid or unknown factor content: application/json: schema: @@ -1517,18 +1424,47 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /sca/login/complete: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer completing an SCA login. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Add a new platform external account - description: Register a new external bank account for the platform. - operationId: createPlatformExternalAccount + summary: Complete an SCA login + description: | + Finalize an SCA login by submitting the proof for the started factor + (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for + `PASSKEY`), echoing the `challengeId` for `SMS_OTP`. Returns the + reported session status. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: completeScaLogin tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1536,46 +1472,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PlatformExternalAccountCreateRequest' - examples: - usBankAccount: - summary: Create external US bank account - value: - currency: USD - accountInfo: - accountType: USD_ACCOUNT - accountNumber: '12345678901' - routingNumber: '123456789' - bankAccountType: CHECKING - bankName: Chase Bank - platformAccountId: ext_acc_123456 - beneficiary: - beneficiaryType: INDIVIDUAL - fullName: John Doe - birthDate: '1990-01-15' - nationality: US - address: - line1: 123 Main Street - city: San Francisco - state: CA - postalCode: '94105' - country: US - sparkWallet: - summary: Create external Spark wallet - value: - currency: BTC - accountInfo: - accountType: SPARK_WALLET - address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu + $ref: '#/components/schemas/ScaLoginCompleteRequest' responses: - '201': - description: External account created successfully + '200': + description: SCA login completed; the session status is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/ScaLoginComplete' '400': - description: Bad request + description: Invalid or expired proof content: application/json: schema: @@ -1586,41 +1492,70 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '409': - description: Conflict - External account already exists + description: SCA is not required for this customer. content: application/json: schema: $ref: '#/components/schemas/Error409' + '423': + description: The customer's login is locked (or suspended) after too many failed attempts. `details.lockedUntil` says when they may retry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error423' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts/{externalAccountId}: + /sca/record-event: parameters: - - name: externalAccountId - in: path - description: System-generated unique external account identifier + - name: customerId + in: query + description: The unique identifier of the customer the security event is recorded for. required: true schema: type: string - get: - summary: Get platform external account by ID - description: Retrieve a platform external account by its system-generated ID - operationId: getPlatformExternalAccountById + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Record a security event + description: | + Record a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view), to + feed adaptive-authentication signals. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: recordSecurityEvent tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RecordSecurityEventRequest' responses: '200': - description: Successful operation + description: Event recorded; the customer's resulting login-security state is returned (including any lockout). content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/RecordSecurityEventResponse' + '400': + description: Invalid event type + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1628,54 +1563,52 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - delete: - summary: Delete platform external account by ID - description: Delete a platform external account by its system-generated ID - operationId: deletePlatformExternalAccountById - tags: - - External Accounts - security: - - BasicAuth: [] - responses: - '204': - description: External account deleted successfully - '401': - description: Unauthorized + '409': + description: SCA is not required for this customer. content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: External account not found + $ref: '#/components/schemas/Error409' + '423': + description: The customer's login is locked (or suspended) after too many failed attempts. `details.lockedUntil` says when they may retry. content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error423' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /beneficial-owners: + /sca/factors/reset: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer resetting a factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Create a beneficial owner + summary: Start a 2FA reset description: | - Add a beneficial owner, director, or company officer to a business customer. The beneficial owner will go through KYC verification automatically. - operationId: createBeneficialOwner + Begin recovering a lost enrolled factor via a liveness-gated, poll-based + flow. Opens the liveness check and returns a `resetId` plus the + opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end + user completes it with. Poll + `GET /sca/factors/reset/{resetId}` until liveness + passes, then call the complete endpoint. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startTwoFactorReset tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1683,16 +1616,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwnerCreateRequest' + $ref: '#/components/schemas/TwoFactorResetStartRequest' responses: '201': - description: Beneficial owner created successfully + description: Reset initiated; the reset handle and liveness material are returned. content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwner' + $ref: '#/components/schemas/TwoFactorResetStart' '400': - description: Bad request - Invalid parameters + description: Invalid or unknown factor content: application/json: schema: @@ -1709,139 +1642,122 @@ paths: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '429': + description: Too many reset attempts. Reset initiation is rate-limited to 5 per 24 hours per customer; retry after the window indicated by `Retry-After`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /sca/factors/reset/{resetId}: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer whose reset status is polled. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: resetId + in: path + description: The reset handle returned by the start call. + required: true + schema: + type: string get: - summary: List beneficial owners + summary: Get 2FA reset status description: | - Retrieve a list of beneficial owners for a business customer. - operationId: listBeneficialOwners + Poll the status of an in-progress 2FA reset until it reaches the liveness-passed value, after which the reset can be completed. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: getTwoFactorResetStatus tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: customerId - in: query - description: The business customer ID - required: true - schema: - type: string - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string responses: '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/BeneficialOwnerListResponse' - '400': - description: Bad request - Invalid parameters + description: The current reset status. content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/TwoFactorResetStatus' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /beneficial-owners/{beneficialOwnerId}: - get: - summary: Get a beneficial owner - description: Retrieve details of a specific beneficial owner by ID. - operationId: getBeneficialOwner - tags: - - KYC/KYB Verifications - security: - - BasicAuth: [] - parameters: - - name: beneficialOwnerId - in: path - description: Beneficial owner ID - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/BeneficialOwner' - '401': - description: Unauthorized + '404': + description: Customer or reset not found content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Beneficial owner not found + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - patch: - summary: Update a beneficial owner - description: Update details of a specific beneficial owner. Only provided fields are updated. - operationId: updateBeneficialOwner + /sca/factors/reset/{resetId}/complete: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer completing the reset. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: resetId + in: path + description: The reset handle returned by the start call. + required: true + schema: + type: string + post: + summary: Complete a 2FA reset + description: | + Complete a 2FA reset once liveness has passed, clearing the lost factor so + the customer can re-enroll. + + For an `SMS_OTP` reset, supply the new `mobile` number in the body — completing + the reset enrolls it as the customer's number. Other factors need no body. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: completeTwoFactorReset tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: beneficialOwnerId - in: path - description: Beneficial owner ID - required: true - schema: - type: string requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwnerUpdateRequest' + $ref: '#/components/schemas/TwoFactorResetCompleteRequest' responses: - '200': - description: Beneficial owner updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/BeneficialOwner' + '204': + description: Reset completed; no content is returned. '400': - description: Bad request - Invalid parameters + description: Reset not ready (liveness not yet passed) content: application/json: schema: @@ -1853,40 +1769,54 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Beneficial owner not found + description: Customer or reset not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /documents: + /customers/external-accounts/{externalAccountId}/trust: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being trusted. + required: true + schema: + type: string post: - summary: Upload a document + summary: Start trusting a beneficiary description: | - Upload a verification document for a customer or beneficial owner. The request must use multipart/form-data with the file in the `file` field and metadata in the remaining fields. + Begin trusting (whitelisting) an external account so future sends to it can + skip the per-transaction SCA ceremony. Returns the `scaChallenge` to satisfy + (when one is issued). Complete with + `POST /customers/external-accounts/{externalAccountId}/trust/confirm`. - Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB. - operationId: uploadDocument + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startBeneficiaryTrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] - requestBody: - $ref: '#/components/requestBodies/DocumentUploadRequestBody' responses: - '201': - description: Document uploaded successfully + '200': + description: Beneficiary trust started; the SCA challenge (if any) is returned. content: application/json: schema: - $ref: '#/components/schemas/Document' + $ref: '#/components/schemas/BeneficiaryTrustStart' '400': - description: Bad request - Invalid file type, size, or parameters + description: Invalid request content: application/json: schema: @@ -1898,57 +1828,62 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Document holder not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - get: - summary: List documents + /customers/external-accounts/{externalAccountId}/trust/confirm: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being trusted. + required: true + schema: + type: string + post: + summary: Confirm trusting a beneficiary description: | - Retrieve a list of documents with optional filtering by document holder. - operationId: listDocuments + Finalize trusting a beneficiary (identified by the `externalAccountId` in the + path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when + one was issued. Returns `trusted: true`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryTrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: documentHolder - in: query - description: Filter by document holder ID (Customer or BeneficialOwner) - required: false - schema: - type: string - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest' responses: '200': - description: Successful operation + description: Beneficiary trusted. content: application/json: schema: - $ref: '#/components/schemas/DocumentListResponse' + $ref: '#/components/schemas/BeneficiaryTrustConfirm' '400': - description: Bad request - Invalid parameters + description: Invalid or expired proof content: application/json: schema: @@ -1959,35 +1894,60 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /documents/{documentId}: - get: - summary: Get a document by ID - description: Retrieve details and metadata of a specific document by ID. - operationId: getDocument + /customers/external-accounts/{externalAccountId}/untrust: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being untrusted. + required: true + schema: + type: string + post: + summary: Start untrusting a beneficiary + description: | + Begin untrusting (removing the trusted mark from) an external account, so + future sends to it are dynamically linked and require the per-transaction SCA + ceremony again. Returns the `scaChallenge` to satisfy (when one is issued). + Complete with + `POST /customers/external-accounts/{externalAccountId}/untrust/confirm`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startBeneficiaryUntrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: documentId - in: path - description: Document ID - required: true - schema: - type: string responses: '200': - description: Successful operation + description: Beneficiary untrust started; the SCA challenge (if any) is returned. content: application/json: schema: - $ref: '#/components/schemas/Document' + $ref: '#/components/schemas/BeneficiaryTrustStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1995,44 +1955,62 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Document not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - put: - summary: Replace a document + /customers/external-accounts/{externalAccountId}/untrust/confirm: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being untrusted. + required: true + schema: + type: string + post: + summary: Confirm untrusting a beneficiary description: | - Replace an existing document with a new file and/or updated metadata. This is useful when a document was rejected and needs to be re-uploaded. The request must use multipart/form-data. - operationId: replaceDocument + Finalize untrusting a beneficiary (identified by the `externalAccountId` in + the path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when + one was issued. Returns `trusted: false`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryUntrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: documentId - in: path - description: Document ID - required: true - schema: - type: string requestBody: - $ref: '#/components/requestBodies/DocumentReplaceRequestBody' + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest' responses: '200': - description: Document replaced successfully + description: Beneficiary untrusted. content: application/json: schema: - $ref: '#/components/schemas/Document' + $ref: '#/components/schemas/BeneficiaryTrustConfirm' '400': - description: Bad request - Invalid file type, size, or parameters + description: Invalid or expired proof content: application/json: schema: @@ -2044,158 +2022,128 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Document not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - delete: - summary: Delete a document + /customers/internal-accounts: + get: + summary: List Customer internal accounts description: | - Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable. - operationId: deleteDocument + Retrieve a list of internal accounts with optional filtering parameters. Returns all + internal accounts that match the specified filters. If no filters are provided, returns all internal accounts + (paginated). + + Internal accounts are created automatically when a customer is created based on the platform configuration. + operationId: listCustomerInternalAccounts tags: - - Documents + - Internal Accounts security: - BasicAuth: [] parameters: - - name: documentId - in: path - description: Document ID - required: true + - name: currency + in: query + description: Filter by currency code + required: false + schema: + type: string + - name: customerId + in: query + description: Filter by internal accounts associated with a specific customer + required: false + schema: + 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. + required: false + schema: + $ref: '#/components/schemas/InternalAccountType' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false schema: type: string responses: - '204': - description: Document deleted successfully - '401': - description: Unauthorized + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Document not found + $ref: '#/components/schemas/InternalAccountListResponse' + '400': + description: Bad request - Invalid parameters content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Document cannot be deleted (already submitted for verification) + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/Error401' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /verifications: - post: - summary: Submit customer for verification + /platform/internal-accounts: + get: + summary: List platform internal accounts description: | - Trigger KYC (individual) or KYB (business) verification for a customer. - The response indicates whether all required information has been provided. - If data is missing, the `errors` array describes exactly what needs to be - supplied before verification can proceed. - - Call this endpoint again after resolving errors to re-submit. - - ### What to collect for KYB - - Before submitting a `BUSINESS` customer, collect the following via - `POST /customers`, `POST /beneficial-owners`, and `POST /documents`: - - **Business identifying information** - - Entity full legal name - - Doing Business As (DBA) name, if applicable - - Physical address — principal place of business - - Countries of operation - - Identification number — U.S. taxpayer identification number, or, for a - foreign business without one, alternative government-issued documentation - certifying the existence of the business - - **Ownership and control structure** — collected for **one control person** - (an individual with significant responsibility to control, manage, or - direct the legal entity) **and all beneficial owners** (every individual - who owns 25% or more, directly or indirectly). For each, provide: - - Full name - - Date of birth - - Address - - Identification number: - - U.S. persons — SSN or ITIN - - Non-U.S. persons — one or more of: ITIN, passport (with country of - issuance), alien identification card, or another government-issued - photo ID evidencing nationality or residence + Retrieve a list of all internal accounts that belong to the platform, as opposed to an individual customer. - **Required documents** - - Company formation and existence documents (certificate of incorporation, - articles of association, etc.) - - Proof of ownership and control structure (organization and ownership - chart, shareholder agreements, operating agreements, register of members, - or certification of controlling person and beneficial owners) - - Proof of address dated within the last 3 months (utility bill, bank - statement, lease agreement, or official correspondence) - - Tax ID or equivalent identifying-number documents - - For non-U.S. beneficial owners — passport plus one additional - government-issued ID - operationId: createVerification + These accounts are created automatically when the platform is configured for each supported currency. They can be used for things like distributing bitcoin rewards to customers, or for other platform-wide purposes. + operationId: listPlatformInternalAccounts tags: - - KYC/KYB Verifications + - Internal Accounts security: - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/VerificationRequest' + parameters: + - name: currency + in: query + description: Filter by currency code + required: false + schema: + 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. + required: false + schema: + $ref: '#/components/schemas/InternalAccountType' responses: '200': - description: | - Verification status returned. Check `verificationStatus` and `errors` to determine next steps. + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Verification' - examples: - missingInfo: - summary: Verification blocked by missing data - value: - id: Verification:019542f5-b3e7-1d02-0000-000000000001 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - verificationStatus: RESOLVE_ERRORS - errors: - - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: MISSING_FIELD - field: customer.address.line1 - reason: Business address line 1 is required - - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: MISSING_PROOF_OF_ADDRESS_DOCUMENT - acceptedDocumentTypes: - - PROOF_OF_ADDRESS - reason: Proof of address document is required - - resourceId: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000002 - type: MISSING_FIELD - field: personalInfo.birthDate - reason: Date of birth is required for beneficial owners - createdAt: '2025-10-03T12:00:00Z' - submitted: - summary: Verification submitted successfully - value: - id: Verification:019542f5-b3e7-1d02-0000-000000000002 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - verificationStatus: IN_PROGRESS - errors: [] - createdAt: '2025-10-03T12:00:00Z' + $ref: '#/components/schemas/PlatformInternalAccountListResponse' '400': description: Bad request - Invalid parameters content: @@ -2208,40 +2156,39 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Customer not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/external-accounts: get: - summary: List verifications + summary: List Customer external accounts description: | - Retrieve a list of verifications with optional filtering by customer ID and status. - operationId: listVerifications + Retrieve a list of external accounts with optional filtering parameters. Returns all + external accounts that match the specified filters. If no filters are provided, returns all external accounts + (paginated). + + External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform. + operationId: listCustomerExternalAccounts tags: - - KYC/KYB Verifications + - External Accounts security: - BasicAuth: [] parameters: - - name: customerId + - name: currency in: query - description: Filter by customer ID + description: Filter by currency code required: false schema: type: string - - name: verificationStatus + - name: customerId in: query - description: Filter by verification status + description: Filter by external accounts associated with a specific customer required: false schema: - $ref: '#/components/schemas/VerificationStatus' + type: string - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -2263,7 +2210,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/VerificationListResponse' + $ref: '#/components/schemas/ExternalAccountListResponse' '400': description: Bad request - Invalid parameters content: @@ -2282,90 +2229,61 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /verifications/{verificationId}: - get: - summary: Get a verification - description: Retrieve details of a specific verification by ID. - operationId: getVerification + post: + summary: Add a new external account + description: Register a new external bank account for a customer. + operationId: createCustomerExternalAccount tags: - - KYC/KYB Verifications + - External Accounts security: - BasicAuth: [] - parameters: - - name: verificationId - in: path - description: Verification ID - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Verification' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Verification not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /transfer-in: - post: - summary: Create a transfer-in request - description: | - Transfer funds from an external account to an internal account for a specific customer. This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the paymentInstructions on the internal account to deposit funds. - operationId: createTransferIn - tags: - - Same-Currency Transfers - security: - - BasicAuth: [] - parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. - schema: - type: string - example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/TransferInRequest' + $ref: '#/components/schemas/ExternalAccountCreateRequest' examples: - transferIn: - summary: Transfer from external to internal account + usBankAccount: + summary: Create external US bank account value: - source: - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - amount: 12550 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + currency: USD + accountInfo: + accountType: USD_ACCOUNT + accountNumber: '12345678901' + routingNumber: '123456789' + bankAccountType: CHECKING + bankName: Chase Bank + platformAccountId: ext_acc_123456 + beneficiary: + beneficiaryType: INDIVIDUAL + fullName: John Doe + birthDate: '1990-01-15' + nationality: US + address: + line1: 123 Main Street + city: San Francisco + state: CA + postalCode: '94105' + country: US + sparkWallet: + summary: Create external Spark wallet + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + currency: BTC + accountInfo: + accountType: SPARK_WALLET + address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu responses: '201': - description: Transfer-in request created successfully + description: External account created successfully content: application/json: schema: - $ref: '#/components/schemas/TransactionOneOf' + $ref: '#/components/schemas/ExternalAccount' '400': - description: Bad request - Invalid parameters + description: Bad request content: application/json: schema: @@ -2376,72 +2294,41 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Customer or account not found + '409': + description: Conflict - External account already exists content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /transfer-out: - post: - summary: Create a transfer-out request - description: | - Transfer funds from an internal account to an external account for a specific customer. - operationId: createTransferOut + /customers/external-accounts/{externalAccountId}: + parameters: + - name: externalAccountId + in: path + description: System-generated unique external account identifier + required: true + schema: + type: string + get: + summary: Get customer external account by ID + description: Retrieve a customer external account by its system-generated ID + operationId: getCustomerExternalAccountById tags: - - Same-Currency Transfers + - External Accounts security: - BasicAuth: [] - parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. - schema: - type: string - example: 550e8400-e29b-41d4-a716-446655440000 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TransferOutRequest' - examples: - transferOut: - summary: Transfer from internal to external account - value: - source: - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - destination: - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - paymentRail: ACH - amount: 12550 - remittanceInformation: '12345' responses: - '201': - description: | - Transfer-out request created successfully. - - For customers outside SCA-regulated regions the transaction proceeds as usual. - - **Strong Customer Authentication (EU):** per-transaction SCA is authorized only on the quote resource (`POST /quotes/{quoteId}/authorize`), and `transfer-out` has no associated quote. `transfer-out` is being deprecated, so SCA-gated EU debits are not offered on this endpoint — use the quote + `execute` flow instead. - content: - application/json: - schema: - $ref: '#/components/schemas/TransactionOneOf' - '400': - description: Bad request - Invalid parameters + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/ExternalAccount' '401': description: Unauthorized content: @@ -2449,7 +2336,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Customer or account not found + description: External account not found content: application/json: schema: @@ -2460,49 +2347,17 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /receiver/uma/{receiverUmaAddress}: - get: - summary: Look up an UMA address for payment - description: | - Lookup a receiving UMA address to determine supported currencies and exchange rates. - This endpoint helps platforms determine what currencies they can send to a given UMA address. - operationId: lookupUma + delete: + summary: Delete customer external account by ID + description: Delete a customer external account by its system-generated ID + operationId: deleteCustomerExternalAccountById tags: - - Cross-Currency Transfers + - External Accounts security: - BasicAuth: [] - parameters: - - name: receiverUmaAddress - in: path - description: UMA address of the intended recipient - required: true - schema: - type: string - - name: senderUmaAddress - in: query - description: UMA address of the sender (optional if customerId is provided) - required: false - schema: - type: string - - name: customerId - in: query - description: System ID of the sender (optional if senderUmaAddress is provided) - required: false - schema: - type: string responses: - '200': - description: Successful lookup - content: - application/json: - schema: - $ref: '#/components/schemas/ReceiverUmaLookupResponse' - '400': - description: Bad request - Missing or invalid parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + '204': + description: External account deleted successfully '401': description: Unauthorized content: @@ -2510,69 +2365,60 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: UMA address not found. The address is well-formed but no receiver exists at the counterparty VASP (`UMA_NOT_FOUND`). + description: External account not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '412': - description: Counterparty doesn't support UMA version - content: - application/json: - schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue - content: - application/json: - schema: - $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /receiver/external-account/{accountId}: + /platform/external-accounts: get: - summary: Look up an external account for payment + summary: List platform external accounts description: | - Lookup an external account by ID to determine supported currencies and exchange rates. - This endpoint helps platforms determine what currencies they can send to a given external account, along with the current estimated exchange rates and minimum and maximum amounts that can be sent. - operationId: lookupExternalAccount + Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer. + + These accounts are used for platform-wide operations such as receiving funds from external sources or managing platform-level payment destinations. + operationId: listPlatformExternalAccounts tags: - - Cross-Currency Transfers + - External Accounts security: - BasicAuth: [] parameters: - - name: accountId - in: path - description: System-generated ID of the external account - required: true + - name: currency + in: query + description: Filter by currency code + required: false schema: type: string - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - - name: senderUmaAddress + - name: limit in: query - description: UMA address of the sender (optional if customerId is provided) + description: Maximum number of results to return (default 20, max 100) required: false schema: - type: string - - name: customerId + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor in: query - description: System ID of the sender (optional if senderUmaAddress is provided) + description: Cursor for pagination (returned from previous request) required: false schema: type: string responses: '200': - description: Successful lookup + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse' + $ref: '#/components/schemas/ExternalAccountListResponse' '400': - description: Bad request - Missing or invalid parameters + description: Bad request - Invalid parameters content: application/json: schema: @@ -2583,56 +2429,110 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: External account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '412': - description: Counterparty doesn't support UMA version - content: - application/json: - schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue - content: - application/json: - schema: - $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes/{quoteId}: - get: - summary: Get quote by ID - description: | - Retrieve a quote by its ID. If the quote has been settled, it will include - the transaction ID. This allows clients to track the full lifecycle of a payment - from quote creation to settlement. - operationId: getQuoteById - tags: - - Cross-Currency Transfers - security: + post: + summary: Add a new platform external account + description: Register a new external bank account for the platform. + operationId: createPlatformExternalAccount + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformExternalAccountCreateRequest' + examples: + usBankAccount: + summary: Create external US bank account + value: + currency: USD + accountInfo: + accountType: USD_ACCOUNT + accountNumber: '12345678901' + routingNumber: '123456789' + bankAccountType: CHECKING + bankName: Chase Bank + platformAccountId: ext_acc_123456 + beneficiary: + beneficiaryType: INDIVIDUAL + fullName: John Doe + birthDate: '1990-01-15' + nationality: US + address: + line1: 123 Main Street + city: San Francisco + state: CA + postalCode: '94105' + country: US + sparkWallet: + summary: Create external Spark wallet + value: + currency: BTC + accountInfo: + accountType: SPARK_WALLET + address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu + responses: + '201': + description: External account created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '409': + description: Conflict - External account already exists + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /platform/external-accounts/{externalAccountId}: + parameters: + - name: externalAccountId + in: path + description: System-generated unique external account identifier + required: true + schema: + type: string + get: + summary: Get platform external account by ID + description: Retrieve a platform external account by its system-generated ID + operationId: getPlatformExternalAccountById + tags: + - External Accounts + security: - BasicAuth: [] - parameters: - - name: quoteId - in: path - description: ID of the quote to retrieve - required: true - schema: - type: string responses: '200': - description: Quote retrieved successfully + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/ExternalAccount' '401': description: Unauthorized content: @@ -2640,7 +2540,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Quote not found + description: External account not found content: application/json: schema: @@ -2651,132 +2551,60 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes: + delete: + summary: Delete platform external account by ID + description: Delete a platform external account by its system-generated ID + operationId: deletePlatformExternalAccountById + tags: + - External Accounts + security: + - BasicAuth: [] + responses: + '204': + description: External account deleted successfully + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: External account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /beneficial-owners: post: - summary: Create a transfer quote + summary: Create a beneficial owner description: | - Generate a quote for a cross-currency transfer between any combination of accounts - and UMA addresses. This endpoint handles currency exchange and provides the necessary - instructions to execute the transfer. - - **Transfer Types Supported:** - - **Account to Account**: Transfer between internal/external accounts with currency exchange. - - **Account to UMA**: Transfer from an internal account to an UMA address. - - **UMA to Account or UMA to UMA**: This transfer type will only be funded by payment instructions, not from an internal account. - - **Key Features:** - - **Flexible Amount Locking**: Always specify whether you want to lock the sending amount or receiving amount - - **Currency Exchange**: Handles all cross-currency transfers with real-time exchange rates - - **Payment Instructions**: For UMA or customer ID sources, provides banking details needed for execution - - **Important:** If you are transferring funds in the same currency (no exchange required), - use the `/transfer-in` or `/transfer-out` endpoints instead. - operationId: createQuote + Add a beneficial owner, director, or company officer to a business customer. The beneficial owner will go through KYC verification automatically. + operationId: createBeneficialOwner tags: - - Cross-Currency Transfers + - KYC/KYB Verifications security: - BasicAuth: [] - parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. - schema: - type: string - example: requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/QuoteRequest' - examples: - accountToAccount: - summary: Account to Account Transfer - value: - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - lockedCurrencySide: SENDING - lockedCurrencyAmount: 10000 - description: Transfer between accounts, either internal or external. - accountToUma: - summary: Account to UMA Address Transfer - value: - lookupId: LookupRequest:019542f5-b3e7-1d02-0000-000000000009 - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: UMA_ADDRESS - umaAddress: $receiver@uma.domain.com - currency: EUR - lockedCurrencySide: SENDING - lockedCurrencyAmount: 1000 - description: 'Payment for invoice #1234' - realTimeFundingToSparkWallet: - summary: Real-time funding to Spark Wallet as an on-ramp flow. Immediate execution. - value: - source: - sourceType: REALTIME_FUNDING - customerId: Customer:019542f5-b3e7-1d02-0000-000000000009 - currency: USD - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456 - lockedCurrencySide: RECEIVING - lockedCurrencyAmount: 10000 - immediatelyExecute: true - description: Bitcoin reward payout! + $ref: '#/components/schemas/BeneficialOwnerCreateRequest' responses: '201': - description: | - Transfer quote created successfully. The response includes exchange rates, - fees, and transfer details. For transfers involving UMA addresses, payment - instructions are also included for execution through banking systems. - content: - application/json: - schema: - $ref: '#/components/schemas/Quote' - example: - id: Quote:019542f5-b3e7-1d02-0000-000000000006 - status: PENDING - createdAt: '2025-10-03T12:00:00Z' - expiresAt: '2025-10-03T12:05:00Z' - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - sendingCurrency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - receivingCurrency: - code: EUR - name: Euro - symbol: € - decimals: 2 - totalSendingAmount: 10000 - totalReceivingAmount: 9200 - exchangeRate: 0.92 - feesIncluded: 10 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005 - '202': - description: | - Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated. + description: Beneficial owner created successfully content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/BeneficialOwner' '400': - description: Bad request - Missing or invalid parameters + description: Bad request - Invalid parameters content: application/json: schema: @@ -2787,172 +2615,145 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '412': - description: Counterparty doesn't support UMA version + '404': + description: Customer not found content: application/json: schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue - content: - application/json: - schema: - $ref: '#/components/schemas/Error424' + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - '501': - description: Not implemented - content: - application/json: - schema: - $ref: '#/components/schemas/Error501' - /quotes/{quoteId}/execute: - post: - summary: Execute a quote + get: + summary: List beneficial owners description: | - Execute a quote by its ID. This endpoint initiates the transfer between - the source and destination accounts. - - This endpoint can only be used for quotes with a `source` which is either an internal account, - or has direct pull functionality (e.g. ACH pull with an external account). - - When the quote's `source` is an internal account of type `EMBEDDED_WALLET`, - the request must include a `Grid-Wallet-Signature` header. The header value - is the full Grid wallet signature built over the `payloadToSign` value from - the quote's `paymentInstructions[].accountOrWalletInfo` entry with the - session private key of a verified authentication credential on the source - Embedded Wallet. - - Once executed, the quote cannot be cancelled and the transfer will be processed. - operationId: executeQuote + Retrieve a list of beneficial owners for a business customer. + operationId: listBeneficialOwners tags: - - Cross-Currency Transfers + - KYC/KYB Verifications security: - BasicAuth: [] parameters: - - name: quoteId - in: path + - name: customerId + in: query + description: The business customer ID required: true - description: The unique identifier of the quote to execute schema: type: string - example: Quote:019542f5-b3e7-1d02-0000-000000000001 - - name: Idempotency-Key - in: header + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. schema: - type: string - example: - - name: Grid-Wallet-Signature - in: header + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) required: false - description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types. schema: type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - requestBody: - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/ExecuteQuoteRequest' responses: '200': - description: | - Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): - - - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). - - - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`. + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/BeneficialOwnerListResponse' '400': - description: Bad request - Invalid quote ID or quote cannot be confirmed + description: Bad request - Invalid parameters content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Quote not found + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Quote already confirmed, expired, or in invalid state + $ref: '#/components/schemas/Error500' + /beneficial-owners/{beneficialOwnerId}: + get: + summary: Get a beneficial owner + description: Retrieve details of a specific beneficial owner by ID. + operationId: getBeneficialOwner + tags: + - KYC/KYB Verifications + security: + - BasicAuth: [] + parameters: + - name: beneficialOwnerId + in: path + description: Beneficial owner ID + required: true + schema: + type: string + responses: + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/BeneficialOwner' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Beneficial owner not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes/{quoteId}/authorize: - parameters: - - name: quoteId - in: path - description: The unique identifier of the quote whose SCA challenge is being authorized. - required: true - schema: - type: string - example: Quote:019542f5-b3e7-1d02-0000-000000000006 - post: - summary: Authorize a quote's SCA challenge - description: | - Satisfy the Strong Customer Authentication challenge carried by a quote in - `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof. - - This is used for realtime-funding quotes: the quote is returned with an - `scaChallenge` and **without** `paymentInstructions`; once authorized, the - quote advances and its `paymentInstructions` are populated so the customer - can fund the transfer. - - As with all SCA, a quote may require more than one authorization: after - authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the - next `scaChallenge` — authorize that too, repeating until it advances (see - `ScaChallenge`). - - This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. - - In sandbox, the SMS code is always `123456`. - operationId: authorizeQuote + patch: + summary: Update a beneficial owner + description: Update details of a specific beneficial owner. Only provided fields are updated. + operationId: updateBeneficialOwner tags: - - Strong Customer Authentication + - KYC/KYB Verifications security: - BasicAuth: [] + parameters: + - name: beneficialOwnerId + in: path + description: Beneficial owner ID + required: true + schema: + type: string requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/ScaAuthorization' + $ref: '#/components/schemas/BeneficialOwnerUpdateRequest' responses: '200': - description: Challenge authorized; the updated quote is returned. + description: Beneficial owner updated successfully content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/BeneficialOwner' '400': - description: Invalid or expired authorization proof + description: Bad request - Invalid parameters content: application/json: schema: @@ -2964,169 +2765,78 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Quote not found + description: Beneficial owner not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '409': - description: SCA is not required for this customer, or the quote has no pending challenge to authorize. - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes/{quoteId}/authorize/resend: - parameters: - - name: quoteId - in: path - description: The unique identifier of the quote whose SCA challenge code should be re-sent. - required: true - schema: - type: string - example: Quote:019542f5-b3e7-1d02-0000-000000000006 + /documents: post: - summary: Resend a quote's SCA challenge code + summary: Upload a document description: | - Re-send the one-time code for a realtime-funding quote in - `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The - existing challenge is reused — no new challenge is issued, and its - `scaChallenge.expiresAt` is **not** extended; once the challenge is past - `expiresAt` it can no longer be authorized. - - Only meaningful for customers in a region where SCA is required (e.g. EU); - a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409. + Upload a verification document for a customer or beneficial owner. The request must use multipart/form-data with the file in the `file` field and metadata in the remaining fields. - In sandbox, the code is always `123456`. - operationId: resendQuoteScaCode + Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB. + operationId: uploadDocument tags: - - Strong Customer Authentication + - Documents security: - BasicAuth: [] + requestBody: + $ref: '#/components/requestBodies/DocumentUploadRequestBody' responses: - '204': - description: Code re-sent. - '401': - description: Unauthorized + '201': + description: Document uploaded successfully content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Quote not found + $ref: '#/components/schemas/Document' + '400': + description: Bad request - Invalid file type, size, or parameters content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/Error409' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + $ref: '#/components/schemas/Error401' + '404': + description: Document holder not found content: application/json: schema: - $ref: '#/components/schemas/Error429' + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions: get: - summary: List transactions + summary: List documents description: | - Retrieve a paginated list of transactions with optional filtering. - The transactions can be filtered by customer ID, platform customer ID, UMA address, - date range, status, and transaction type. - - Card transactions are included and identified by `type: CARD`. In Sandbox this is how - you discover a `CardTransaction` id after simulating an authorization — list the - transactions, take the card transaction's `id`, and pass it as the `cardTransactionId` - to the clearing and return simulate endpoints. - operationId: listTransactions - tags: - - Transactions + Retrieve a list of documents with optional filtering by document holder. + operationId: listDocuments + tags: + - Documents security: - BasicAuth: [] parameters: - - name: customerId - in: query - description: Filter by system customer ID. To filter to transactions made on behalf of the platform, specify the platform ID as the customer ID. - required: false - schema: - type: string - - name: platformCustomerId - in: query - description: Filter by platform-specific customer ID - required: false - schema: - type: string - - name: accountIdentifier - in: query - description: Filter by account identifier (matches either sender or receiver) - required: false - schema: - type: string - - name: senderAccountIdentifier - in: query - description: Filter by sender account identifier - required: false - schema: - type: string - - name: receiverAccountIdentifier - in: query - description: Filter by receiver account identifier - required: false - schema: - type: string - - name: status - in: query - description: Filter by transaction status - required: false - schema: - $ref: '#/components/schemas/TransactionStatus' - - name: type - in: query - description: Filter by transaction type - required: false - schema: - $ref: '#/components/schemas/TransactionType' - - name: reference - in: query - description: Filter by reference - required: false - schema: - type: string - - name: startDate - in: query - description: Filter by start date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: endDate + - name: documentHolder in: query - description: Filter by end date (inclusive) in ISO 8601 format + description: Filter by document holder ID (Customer or BeneficialOwner) required: false schema: type: string - format: date-time - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -3142,23 +2852,13 @@ paths: required: false schema: type: string - - name: sortOrder - in: query - description: Order to sort results in - required: false - schema: - type: string - enum: - - asc - - desc - default: desc responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/TransactionListResponse' + $ref: '#/components/schemas/DocumentListResponse' '400': description: Bad request - Invalid parameters content: @@ -3177,29 +2877,29 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}: - parameters: - - name: transactionId - in: path - description: Unique identifier of the transaction - required: true - schema: - type: string + /documents/{documentId}: get: - summary: Get transaction by ID - description: Retrieve detailed information about a specific transaction. - operationId: getTransactionById + summary: Get a document by ID + description: Retrieve details and metadata of a specific document by ID. + operationId: getDocument tags: - - Transactions + - Documents security: - BasicAuth: [] + parameters: + - name: documentId + in: path + description: Document ID + required: true + schema: + type: string responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/TransactionOneOf' + $ref: '#/components/schemas/Document' '401': description: Unauthorized content: @@ -3207,7 +2907,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: @@ -3218,38 +2918,33 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/confirm: - post: - summary: Confirm receipt delivery + put: + summary: Replace a document description: | - Confirm that the platform delivered the transaction receipt to its customer. This confirmation is only necessary when the platform is contractually required to send a receipt. If `receiptDeliveryConfirmedAt` is omitted, the confirmation time is set to the current server time. Calling this endpoint again for the same transaction updates the stored confirmation time. - operationId: confirmReceiptDelivery + Replace an existing document with a new file and/or updated metadata. This is useful when a document was rejected and needs to be re-uploaded. The request must use multipart/form-data. + operationId: replaceDocument tags: - - Transactions + - Documents security: - BasicAuth: [] parameters: - - name: transactionId + - name: documentId in: path - description: Unique identifier of the transaction to confirm receipt delivery for + description: Document ID required: true schema: type: string requestBody: - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/ConfirmReceiptDeliveryRequest' + $ref: '#/components/requestBodies/DocumentReplaceRequestBody' responses: '200': - description: Receipt delivery confirmation recorded successfully + description: Document replaced successfully content: application/json: schema: - $ref: '#/components/schemas/TransactionOneOf' + $ref: '#/components/schemas/Document' '400': - description: Bad request - Invalid parameters + description: Bad request - Invalid file type, size, or parameters content: application/json: schema: @@ -3261,7 +2956,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: @@ -3272,43 +2967,25 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/approve: - post: - summary: Approve a pending incoming payment + delete: + summary: Delete a document description: | - Approve a pending incoming payment that was previously acknowledged with a 202 response. - This endpoint allows platforms to asynchronously approve payments after async processing. - operationId: approvePendingPayment + Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable. + operationId: deleteDocument tags: - - Transactions + - Documents security: - BasicAuth: [] parameters: - - name: transactionId + - name: documentId in: path - description: Unique identifier of the transaction to approve + description: Document ID required: true schema: type: string - requestBody: - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/ApprovePaymentRequest' responses: - '200': - description: Payment approved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/IncomingTransaction' - '400': - description: Bad request - Invalid parameters or payment cannot be approved - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + '204': + description: Document deleted successfully '401': description: Unauthorized content: @@ -3316,13 +2993,13 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: Conflict - Payment is not in a pending state or has already been processed or timed out. + description: Conflict - Document cannot be deleted (already submitted for verification) content: application/json: schema: @@ -3333,99 +3010,172 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/reject: + /verifications: post: - summary: Reject a pending incoming payment + summary: Submit customer for verification description: | - Reject a pending incoming payment that was previously acknowledged with a 202 response. - This endpoint allows platforms to asynchronously reject payments after additional processing. - operationId: rejectPendingPayment + Trigger KYC (individual) or KYB (business) verification for a customer. + The response indicates whether all required information has been provided. + If data is missing, the `errors` array describes exactly what needs to be + supplied before verification can proceed. + + Call this endpoint again after resolving errors to re-submit. + + ### What to collect for KYB + + Before submitting a `BUSINESS` customer, collect the following via + `POST /customers`, `POST /beneficial-owners`, and `POST /documents`: + + **Business identifying information** + - Entity full legal name + - Doing Business As (DBA) name, if applicable + - Physical address — principal place of business + - Countries of operation + - Identification number — U.S. taxpayer identification number, or, for a + foreign business without one, alternative government-issued documentation + certifying the existence of the business + + **Ownership and control structure** — collected for **one control person** + (an individual with significant responsibility to control, manage, or + direct the legal entity) **and all beneficial owners** (every individual + who owns 25% or more, directly or indirectly). For each, provide: + - Full name + - Date of birth + - Address + - Identification number: + - U.S. persons — SSN or ITIN + - Non-U.S. persons — one or more of: ITIN, passport (with country of + issuance), alien identification card, or another government-issued + photo ID evidencing nationality or residence + + **Required documents** + - Company formation and existence documents (certificate of incorporation, + articles of association, etc.) + - Proof of ownership and control structure (organization and ownership + chart, shareholder agreements, operating agreements, register of members, + or certification of controlling person and beneficial owners) + - Proof of address dated within the last 3 months (utility bill, bank + statement, lease agreement, or official correspondence) + - Tax ID or equivalent identifying-number documents + - For non-U.S. beneficial owners — passport plus one additional + government-issued ID + operationId: createVerification tags: - - Transactions + - KYC/KYB Verifications security: - BasicAuth: [] - parameters: - - name: transactionId - in: path - description: Unique identifier of the transaction to reject - required: true - schema: - type: string requestBody: - required: false + required: true content: application/json: schema: - $ref: '#/components/schemas/RejectPaymentRequest' + $ref: '#/components/schemas/VerificationRequest' responses: '200': - description: Payment rejected successfully - content: - application/json: - schema: - $ref: '#/components/schemas/IncomingTransaction' - '400': - description: Bad request - Invalid parameters or payment cannot be rejected - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized + description: | + Verification status returned. Check `verificationStatus` and `errors` to determine next steps. content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Transaction not found + $ref: '#/components/schemas/Verification' + examples: + missingInfo: + summary: Verification blocked by missing data + value: + id: Verification:019542f5-b3e7-1d02-0000-000000000001 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + verificationStatus: RESOLVE_ERRORS + errors: + - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: MISSING_FIELD + field: customer.address.line1 + reason: Business address line 1 is required + - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: MISSING_PROOF_OF_ADDRESS_DOCUMENT + acceptedDocumentTypes: + - PROOF_OF_ADDRESS + reason: Proof of address document is required + - resourceId: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000002 + type: MISSING_FIELD + field: personalInfo.birthDate + reason: Date of birth is required for beneficial owners + createdAt: '2025-10-03T12:00:00Z' + submitted: + summary: Verification submitted successfully + value: + id: Verification:019542f5-b3e7-1d02-0000-000000000002 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + verificationStatus: IN_PROGRESS + errors: [] + createdAt: '2025-10-03T12:00:00Z' + '400': + description: Bad request - Invalid parameters content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Payment is not in a pending state or has already been processed or timed out. + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /crypto/estimate-withdrawal-fee: - post: - summary: Estimate crypto withdrawal fee + get: + summary: List verifications description: | - Estimate the network and application fees for a cryptocurrency withdrawal from a crypto internal account to an external blockchain address. Use this to show fee information to customers before they initiate a withdrawal. - operationId: estimateCryptoWithdrawalFee + Retrieve a list of verifications with optional filtering by customer ID and status. + operationId: listVerifications tags: - - Cross-Currency Transfers + - KYC/KYB Verifications security: - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeRequest' - examples: - estimateUSDCWithdrawal: - summary: Estimate fee for USDC withdrawal on Solana - value: - internalAccountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - currency: USDC - cryptoNetwork: SOLANA - amount: 1000000 - destinationAddress: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU + parameters: + - name: customerId + in: query + description: Filter by customer ID + required: false + schema: + type: string + - name: verificationStatus + in: query + description: Filter by verification status + required: false + schema: + $ref: '#/components/schemas/VerificationStatus' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string responses: '200': - description: Fee estimation returned successfully + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeResponse' + $ref: '#/components/schemas/VerificationListResponse' '400': description: Bad request - Invalid parameters content: @@ -3438,216 +3188,164 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Internal account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/webhooks/test: - post: - summary: Send a test webhook - description: Send a test webhook to the configured endpoint - operationId: sendTestWebhook + /verifications/{verificationId}: + get: + summary: Get a verification + description: Retrieve details of a specific verification by ID. + operationId: getVerification tags: - - Sandbox + - KYC/KYB Verifications security: - BasicAuth: [] + parameters: + - name: verificationId + in: path + description: Verification ID + required: true + schema: + type: string responses: '200': - description: Webhook delivered successfully - content: - application/json: - schema: - $ref: '#/components/schemas/TestWebhookResponse' - '400': - description: Bad request - Webhook delivery failed + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/Verification' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Verification not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/bulk/csv: + /transfer-in: post: - summary: Upload customers via CSV file + summary: Create a transfer-in request description: | - Upload a CSV file containing customer information for bulk creation. The CSV file should follow - a specific format with required and optional columns based on customer type. - - ### CSV Format - The CSV file should have the following columns: - - Required columns for all customers: - - umaAddress: The customer's UMA address (e.g., $john.doe@uma.domain.com) - - platformCustomerId: Your platform's unique identifier for the customer - - customerType: Either "INDIVIDUAL" or "BUSINESS" - - Required columns for individual customers: - - fullName: Individual's full name - - birthDate: Date of birth in YYYY-MM-DD format - - addressLine1: Street address line 1 - - city: City - - state: State/Province/Region - - postalCode: Postal/ZIP code - - country: Country code (ISO 3166-1 alpha-2) - - Required columns for business customers: - - businessLegalName: Legal name of the business - - addressLine1: Street address line 1 - - city: City - - state: State/Province/Region - - postalCode: Postal/ZIP code - - country: Country code (ISO 3166-1 alpha-2) - - Optional columns for all customers: - - addressLine2: Street address line 2 - - platformAccountId: Your platform's identifier for the bank account - - description: Optional description for the customer - - Optional columns for individual customers: - - email: Customer's email address - - Optional columns for business customers: - - businessRegistrationNumber: Business registration number - - businessTaxId: Tax identification number - - ### Example CSV - ```csv - umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,platformAccountId,businessLegalName - john.doe@uma.domain.com,customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US - acme@uma.domain.com,biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US - ``` - - The upload process is asynchronous and will return a job ID that can be used to track progress. - You can monitor the job status using the `/customers/bulk/jobs/{jobId}` endpoint. - operationId: uploadCustomersCsv + Transfer funds from an external account to an internal account for a specific customer. This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the paymentInstructions on the internal account to deposit funds. + operationId: createTransferIn tags: - - Customers + - Same-Currency Transfers security: - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: - multipart/form-data: + application/json: schema: - type: object - required: - - file - properties: - file: - type: string - format: binary - description: CSV file containing customer information + $ref: '#/components/schemas/TransferInRequest' + examples: + transferIn: + summary: Transfer from external to internal account + value: + source: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + amount: 12550 responses: - '202': - description: CSV upload accepted for processing + '201': + description: Transfer-in request created successfully content: application/json: schema: - $ref: '#/components/schemas/BulkCustomerImportJobAccepted' + $ref: '#/components/schemas/TransactionOneOf' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer or account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/bulk/jobs/{jobId}: - get: - summary: Get bulk import job status + /transfer-out: + post: + summary: Create a transfer-out request description: | - Retrieve the current status and results of a bulk customer import job. This endpoint can be used - to track the progress of both CSV uploads. - - The response includes: - - Overall job status - - Progress statistics - - Detailed error information for failed entries - - Completion timestamp when finished - operationId: getBulkCustomerImportJob + Transfer funds from an internal account to an external account for a specific customer. + operationId: createTransferOut tags: - - Customers + - Same-Currency Transfers security: - BasicAuth: [] parameters: - - name: jobId - in: path - description: ID of the bulk import job to retrieve - required: true + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. schema: type: string - responses: - '200': - description: Job status retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/BulkCustomerImportJob' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Job not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /invitations: - post: - summary: Create an UMA invitation - description: | - Create an UMA invitation from a given platform customer. - operationId: createInvitation - tags: - - Invitations - security: - - BasicAuth: [] + example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/UmaInvitationCreateRequest' + $ref: '#/components/schemas/TransferOutRequest' + examples: + transferOut: + summary: Transfer from internal to external account + value: + source: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + destination: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: ACH + amount: 12550 + remittanceInformation: '12345' responses: '201': - description: Invitation created successfully + description: | + Transfer-out request created successfully. content: application/json: schema: - $ref: '#/components/schemas/UmaInvitation' + $ref: '#/components/schemas/TransactionOneOf' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: @@ -3658,36 +3356,61 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer or account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /invitations/{invitationCode}: + /receiver/uma/{receiverUmaAddress}: get: - summary: Get an UMA invitation by code + summary: Look up an UMA address for payment description: | - Retrieve details about an UMA invitation by its invitation code. - operationId: getInvitation + Lookup a receiving UMA address to determine supported currencies and exchange rates. + This endpoint helps platforms determine what currencies they can send to a given UMA address. + operationId: lookupUma tags: - - Invitations + - Cross-Currency Transfers security: - BasicAuth: [] parameters: - - name: invitationCode + - name: receiverUmaAddress in: path - description: The code of the invitation to get + description: UMA address of the intended recipient required: true schema: type: string + - name: senderUmaAddress + in: query + description: UMA address of the sender (optional if customerId is provided) + required: false + schema: + type: string + - name: customerId + in: query + description: System ID of the sender (optional if senderUmaAddress is provided) + required: false + schema: + type: string responses: '200': - description: Invitation retrieved successfully + description: Successful lookup content: application/json: schema: - $ref: '#/components/schemas/UmaInvitation' + $ref: '#/components/schemas/ReceiverUmaLookupResponse' + '400': + description: Bad request - Missing or invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -3695,56 +3418,69 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Invitation not found + description: UMA address not found. The address is well-formed but no receiver exists at the counterparty VASP (`UMA_NOT_FOUND`). content: application/json: schema: $ref: '#/components/schemas/Error404' + '412': + description: Counterparty doesn't support UMA version + content: + application/json: + schema: + $ref: '#/components/schemas/Error412' + '424': + description: Counterparty issue + content: + application/json: + schema: + $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /invitations/{invitationCode}/claim: - post: - summary: Claim an UMA invitation + /receiver/external-account/{accountId}: + get: + summary: Look up an external account for payment description: | - Claim an UMA invitation by associating it with an invitee UMA address. - - When an invitation is successfully claimed: - 1. The invitation status changes from PENDING to CLAIMED - 2. The invitee UMA address is associated with the invitation - 3. An INVITATION_CLAIMED webhook is triggered to notify the platform that created the invitation - - This endpoint allows customers to accept invitations sent to them by other UMA customers. - operationId: claimInvitation + Lookup an external account by ID to determine supported currencies and exchange rates. + This endpoint helps platforms determine what currencies they can send to a given external account, along with the current estimated exchange rates and minimum and maximum amounts that can be sent. + operationId: lookupExternalAccount tags: - - Invitations + - Cross-Currency Transfers security: - BasicAuth: [] parameters: - - name: invitationCode + - name: accountId in: path - description: The code of the invitation to claim + description: System-generated ID of the external account required: true schema: type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UmaInvitationClaimRequest' + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + - name: senderUmaAddress + in: query + description: UMA address of the sender (optional if customerId is provided) + required: false + schema: + type: string + - name: customerId + in: query + description: System ID of the sender (optional if senderUmaAddress is provided) + required: false + schema: + type: string responses: '200': - description: Invitation claimed successfully + description: Successful lookup content: application/json: schema: - $ref: '#/components/schemas/UmaInvitation' + $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse' '400': - description: Bad request + description: Bad request - Missing or invalid parameters content: application/json: schema: @@ -3756,69 +3492,63 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Invitation not found + description: External account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '412': + description: Counterparty doesn't support UMA version + content: + application/json: + schema: + $ref: '#/components/schemas/Error412' + '424': + description: Counterparty issue + content: + application/json: + schema: + $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /invitations/{invitationCode}/cancel: - post: - summary: Cancel an UMA invitation + /quotes/{quoteId}: + get: + summary: Get quote by ID description: | - Cancel a pending UMA invitation. Only the inviter or platform can cancel an invitation. - - When an invitation is cancelled: - 1. The invitation status changes from PENDING to CANCELLED - 2. The invitation can no longer be claimed - 3. The invitation URL will show as cancelled when accessed - - Only pending invitations can be cancelled. Attempting to cancel an invitation - that is already claimed, expired, or cancelled will result in an error. - operationId: cancelInvitation + Retrieve a quote by its ID. If the quote has been settled, it will include + the transaction ID. This allows clients to track the full lifecycle of a payment + from quote creation to settlement. + operationId: getQuoteById tags: - - Invitations + - Cross-Currency Transfers security: - BasicAuth: [] parameters: - - name: invitationCode + - name: quoteId in: path - description: The code of the invitation to cancel + description: ID of the quote to retrieve required: true schema: type: string responses: '200': - description: Invitation cancelled successfully - content: - application/json: - schema: - $ref: '#/components/schemas/UmaInvitation' - '400': - description: Bad request - Invitation cannot be cancelled (already claimed, expired, or cancelled) + description: Quote retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/Quote' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Only the platform which created the invitation can cancel it - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Invitation not found + description: Quote not found content: application/json: schema: @@ -3829,32 +3559,132 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/send: + /quotes: post: - summary: Simulate sending funds + summary: Create a transfer quote description: | - Simulate sending funds to the bank account as instructed in the quote. - This endpoint is only for the sandbox environment and will fail for production platforms/keys. - operationId: sandboxSend - tags: - - Sandbox - security: - - BasicAuth: [] + Generate a quote for a cross-currency transfer between any combination of accounts + and UMA addresses. This endpoint handles currency exchange and provides the necessary + instructions to execute the transfer. + + **Transfer Types Supported:** + - **Account to Account**: Transfer between internal/external accounts with currency exchange. + - **Account to UMA**: Transfer from an internal account to an UMA address. + - **UMA to Account or UMA to UMA**: This transfer type will only be funded by payment instructions, not from an internal account. + + **Key Features:** + - **Flexible Amount Locking**: Always specify whether you want to lock the sending amount or receiving amount + - **Currency Exchange**: Handles all cross-currency transfers with real-time exchange rates + - **Payment Instructions**: For UMA or customer ID sources, provides banking details needed for execution + + **Important:** If you are transferring funds in the same currency (no exchange required), + use the `/transfer-in` or `/transfer-out` endpoints instead. + operationId: createQuote + tags: + - Cross-Currency Transfers + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/SandboxSendRequest' + $ref: '#/components/schemas/QuoteRequest' + examples: + accountToAccount: + summary: Account to Account Transfer + value: + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + lockedCurrencySide: SENDING + lockedCurrencyAmount: 10000 + description: Transfer between accounts, either internal or external. + accountToUma: + summary: Account to UMA Address Transfer + value: + lookupId: LookupRequest:019542f5-b3e7-1d02-0000-000000000009 + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: UMA_ADDRESS + umaAddress: $receiver@uma.domain.com + currency: EUR + lockedCurrencySide: SENDING + lockedCurrencyAmount: 1000 + description: 'Payment for invoice #1234' + realTimeFundingToSparkWallet: + summary: Real-time funding to Spark Wallet as an on-ramp flow. Immediate execution. + value: + source: + sourceType: REALTIME_FUNDING + customerId: Customer:019542f5-b3e7-1d02-0000-000000000009 + currency: USD + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456 + lockedCurrencySide: RECEIVING + lockedCurrencyAmount: 10000 + immediatelyExecute: true + description: Bitcoin reward payout! responses: - '200': - description: Funds received successfully + '201': + description: | + Transfer quote created successfully. The response includes exchange rates, + fees, and transfer details. For transfers involving UMA addresses, payment + instructions are also included for execution through banking systems. content: application/json: schema: - $ref: '#/components/schemas/OutgoingTransaction' + $ref: '#/components/schemas/Quote' + example: + id: Quote:019542f5-b3e7-1d02-0000-000000000006 + status: PENDING + createdAt: '2025-10-03T12:00:00Z' + expiresAt: '2025-10-03T12:05:00Z' + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + sendingCurrency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + receivingCurrency: + code: EUR + name: Euro + symbol: € + decimals: 2 + totalSendingAmount: 10000 + totalReceivingAmount: 9200 + exchangeRate: 0.92 + feesIncluded: 10 + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005 + '202': + description: | + Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated. + content: + application/json: + schema: + $ref: '#/components/schemas/Quote' '400': - description: Bad request + description: Bad request - Missing or invalid parameters content: application/json: schema: @@ -3865,121 +3695,172 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token + '412': + description: Counterparty doesn't support UMA version content: application/json: schema: - $ref: '#/components/schemas/Error403' - '404': - description: Quote not found + $ref: '#/components/schemas/Error412' + '424': + description: Counterparty issue content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/uma/receive: + '501': + description: Not implemented + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /quotes/{quoteId}/execute: post: - summary: Simulate payment send to test receiving an UMA payment + summary: Execute a quote description: | - Simulate sending payment from an sandbox uma address to a platform customer to test payment receive. - This endpoint is only for the sandbox environment and will fail for production platforms/keys. - operationId: sandboxReceive + Execute a quote by its ID. This endpoint initiates the transfer between + the source and destination accounts. + + This endpoint can only be used for quotes with a `source` which is either an internal account, + or has direct pull functionality (e.g. ACH pull with an external account). + + When the quote's `source` is an internal account of type `EMBEDDED_WALLET`, + the request must include a `Grid-Wallet-Signature` header. The header value + is the full Grid wallet signature built over the `payloadToSign` value from + the quote's `paymentInstructions[].accountOrWalletInfo` entry with the + session private key of a verified authentication credential on the source + Embedded Wallet. + + Once executed, the quote cannot be cancelled and the transfer will be processed. + operationId: executeQuote tags: - - Sandbox + - Cross-Currency Transfers security: - BasicAuth: [] + parameters: + - name: quoteId + in: path + required: true + description: The unique identifier of the quote to execute + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000001 + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: + - name: Grid-Wallet-Signature + in: header + required: false + description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/SandboxUmaReceiveRequest' + $ref: '#/components/schemas/ExecuteQuoteRequest' responses: '200': - description: Payment triggered successfully + description: | + Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): + + - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). + + - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`. content: application/json: schema: - $ref: '#/components/schemas/IncomingTransaction' + $ref: '#/components/schemas/Quote' '400': - description: Bad request + description: Bad request - Invalid quote ID or quote cannot be confirmed content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`. content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token + '404': + description: Quote not found content: application/json: schema: - $ref: '#/components/schemas/Error403' - '404': - description: Sender or receiver not found + $ref: '#/components/schemas/Error404' + '409': + description: Conflict - Quote already confirmed, expired, or in invalid state content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/internal-accounts/{accountId}/fund: + /quotes/{quoteId}/authorize: + parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge is being authorized. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 post: - summary: Simulate funding an internal account + summary: Authorize a quote's SCA challenge description: | - Simulate receiving funds into an internal account in the sandbox environment. This is useful for testing scenarios where you need to add funds to a customer's or platform's internal account without going through a real bank transfer or following payment instructions. - This endpoint is only for the sandbox environment and will fail for production platforms/keys. - operationId: sandboxFundInternalAccount + Satisfy the Strong Customer Authentication challenge carried by a quote in + `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof. + + This is used for realtime-funding quotes: the quote is returned with an + `scaChallenge` and **without** `paymentInstructions`; once authorized, the + quote advances and its `paymentInstructions` are populated so the customer + can fund the transfer. + + As with all SCA, a quote may require more than one authorization: after + authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the + next `scaChallenge` — authorize that too, repeating until it advances (see + `ScaChallenge`). + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS code is always `123456`. + operationId: authorizeQuote tags: - - Sandbox + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: accountId - in: path - required: true - description: The ID of the internal account to fund - schema: - type: string - example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/SandboxFundRequest' - examples: - fundUSDAccount: - summary: Fund USD internal account with $1,000 - value: - amount: 100000 - fundBTCAccount: - summary: Fund BTC internal account with 0.01 BTC - value: - amount: 1000000 + $ref: '#/components/schemas/ScaAuthorization' responses: '200': - description: Internal account funded successfully + description: Challenge authorized; the updated quote is returned. content: application/json: schema: - $ref: '#/components/schemas/InternalAccount' + $ref: '#/components/schemas/Quote' '400': - description: Bad request + description: Invalid or expired authorization proof content: application/json: schema: @@ -3990,180 +3871,166 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token + '404': + description: Quote not found content: application/json: schema: - $ref: '#/components/schemas/Error403' - '404': - description: Internal account not found + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer, or the quote has no pending challenge to authorize. content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /uma-providers: - get: - summary: List available Counterparty Providers + /quotes/{quoteId}/authorize/resend: + parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge code should be re-sent. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 + post: + summary: Resend a quote's SCA challenge code description: | - Retrieve a list of available Counterparty Providers. The response includes basic information about each provider, such as its UMA address, name, and supported currencies. - operationId: getAvailableUmaProviders + Re-send the one-time code for a realtime-funding quote in + `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The + existing challenge is reused — no new challenge is issued, and its + `scaChallenge.expiresAt` is **not** extended; once the challenge is past + `expiresAt` it can no longer be authorized. + + Only meaningful for customers in a region where SCA is required (e.g. EU); + a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409. + + In sandbox, the code is always `123456`. + operationId: resendQuoteScaCode tags: - - Available UMA Providers - parameters: - - in: query - name: countryCode - description: The alpha-2 representation of a country, as defined by the ISO 3166-1 standard. - required: false - schema: - type: string - example: US - - in: query - name: currencyCode - description: The ISO 4217 currency code to filter providers by supported currency. - required: false - schema: - type: string - example: USD - - in: query - name: hasBlockedProviders - description: Whether to include providers which are not on your allowlist in the response. By default the response will include blocked providers. - required: false - schema: - type: boolean - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string - - name: sortOrder - in: query - description: Order to sort results in - required: false - schema: - type: string - enum: - - asc - - desc - default: desc + - Strong Customer Authentication security: - BasicAuth: [] responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/UmaProviderListResponse' + '204': + description: Code re-sent. '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /tokens: - post: - summary: Create a new API token - description: Create a new API token to access the Grid APIs. - operationId: createToken - tags: - - API Tokens - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ApiTokenCreateRequest' - responses: - '201': - description: API token created successfully + '404': + description: Quote not found content: application/json: schema: - $ref: '#/components/schemas/ApiToken' - '400': - description: Bad request + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). content: application/json: schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized + $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. content: application/json: schema: - $ref: '#/components/schemas/Error401' + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /transactions: get: - summary: List tokens + summary: List transactions description: | - Retrieve a list of API tokens with optional filtering parameters. Returns all tokens that match - the specified filters. If no filters are provided, returns all tokens (paginated). - operationId: listTokens + Retrieve a paginated list of transactions with optional filtering. + The transactions can be filtered by customer ID, platform customer ID, UMA address, + date range, status, and transaction type. + + Card transactions are included and identified by `type: CARD`. In Sandbox this is how + you discover a `CardTransaction` id after simulating an authorization — list the + transactions, take the card transaction's `id`, and pass it as the `cardTransactionId` + to the clearing and return simulate endpoints. + operationId: listTransactions tags: - - API Tokens + - Transactions security: - BasicAuth: [] parameters: - - name: name + - name: customerId in: query - description: Filter by name of the token + description: Filter by system customer ID. To filter to transactions made on behalf of the platform, specify the platform ID as the customer ID. required: false schema: type: string - - name: createdAfter + - name: platformCustomerId in: query - description: Filter customers created after this timestamp (inclusive) + description: Filter by platform-specific customer ID required: false schema: type: string - format: date-time - - name: createdBefore + - name: accountIdentifier in: query - description: Filter customers created before this timestamp (inclusive) + description: Filter by account identifier (matches either sender or receiver) required: false schema: type: string - format: date-time - - name: updatedAfter + - name: senderAccountIdentifier in: query - description: Filter customers updated after this timestamp (inclusive) + description: Filter by sender account identifier + required: false + schema: + type: string + - name: receiverAccountIdentifier + in: query + description: Filter by receiver account identifier + required: false + schema: + type: string + - name: status + in: query + description: Filter by transaction status + required: false + schema: + $ref: '#/components/schemas/TransactionStatus' + - name: type + in: query + description: Filter by transaction type + required: false + schema: + $ref: '#/components/schemas/TransactionType' + - name: reference + in: query + description: Filter by reference + required: false + schema: + type: string + - name: startDate + in: query + description: Filter by start date (inclusive) in ISO 8601 format required: false schema: type: string format: date-time - - name: updatedBefore + - name: endDate in: query - description: Filter customers updated before this timestamp (inclusive) + description: Filter by end date (inclusive) in ISO 8601 format required: false schema: type: string @@ -4183,13 +4050,23 @@ paths: required: false schema: type: string + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/TokenListResponse' + $ref: '#/components/schemas/TransactionListResponse' '400': description: Bad request - Invalid parameters content: @@ -4208,20 +4085,20 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /tokens/{tokenId}: + /transactions/{transactionId}: parameters: - - name: tokenId + - name: transactionId in: path - description: System-generated unique token identifier + description: Unique identifier of the transaction required: true schema: type: string get: - summary: Get API token by ID - description: Retrieve an API token by their system-generated ID - operationId: getTokenById + summary: Get transaction by ID + description: Retrieve detailed information about a specific transaction. + operationId: getTransactionById tags: - - API Tokens + - Transactions security: - BasicAuth: [] responses: @@ -4230,7 +4107,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiToken' + $ref: '#/components/schemas/TransactionOneOf' '401': description: Unauthorized content: @@ -4238,7 +4115,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Token not found + description: Transaction not found content: application/json: schema: @@ -4249,19 +4126,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - delete: - summary: Delete API token by ID - description: Delete an API token by their system-generated ID - operationId: deleteTokenById + /transactions/{transactionId}/confirm: + post: + summary: Confirm receipt delivery + description: | + Confirm that the platform delivered the transaction receipt to its customer. This confirmation is only necessary when the platform is contractually required to send a receipt. If `receiptDeliveryConfirmedAt` is omitted, the confirmation time is set to the current server time. Calling this endpoint again for the same transaction updates the stored confirmation time. + operationId: confirmReceiptDelivery tags: - - API Tokens + - Transactions security: - BasicAuth: [] + parameters: + - name: transactionId + in: path + description: Unique identifier of the transaction to confirm receipt delivery for + required: true + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/ConfirmReceiptDeliveryRequest' responses: - '204': - description: API token deleted successfully + '200': + description: Receipt delivery confirmation recorded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionOneOf' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: @@ -4273,7 +4169,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Token not found + description: Transaction not found content: application/json: schema: @@ -4284,368 +4180,168 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /internal-accounts/{id}: - parameters: - - name: id - in: path - description: The id of the internal account to update. - required: true - schema: - type: string - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - patch: - summary: Update internal account + /transactions/{transactionId}/approve: + post: + summary: Approve a pending incoming payment description: | - Update mutable fields on an internal account. Today this supports updating the wallet privacy setting for an Embedded Wallet internal account. - - Updating wallet privacy is a two-step signed-retry flow: - - 1. Call `PATCH /internal-accounts/{id}` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. - - 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same update fields submitted in step 1. The signed retry returns `200` with the updated internal account. - operationId: updateInternalAccount + Approve a pending incoming payment that was previously acknowledged with a 202 response. + This endpoint allows platforms to asynchronously approve payments after async processing. + operationId: approvePendingPayment tags: - - Internal Accounts + - Transactions security: - BasicAuth: [] parameters: - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + - name: transactionId + in: path + description: Unique identifier of the transaction to approve + required: true schema: type: string - example: Request:019542f5-b3e7-1d02-0000-000000000010 requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/InternalAccountUpdateRequest' - examples: - updateWalletPrivacy: - summary: Update wallet privacy request (both steps) - value: - privateEnabled: true + $ref: '#/components/schemas/ApprovePaymentRequest' responses: '200': - description: Signed retry accepted. Returns the updated internal account. - content: - application/json: - schema: - $ref: '#/components/schemas/InternalAccount' - examples: - enabled: - summary: Wallet privacy enabled - value: - id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: EMBEDDED_WALLET - status: ACTIVE - balance: - amount: 12550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - totalBalance: - amount: 12550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - fundingPaymentInstructions: [] - privateEnabled: true - createdAt: '2026-04-08T15:30:00Z' - updatedAt: '2026-04-08T15:35:02Z' - '202': - description: Challenge issued. The response contains `payloadToSign` (which binds the submitted update fields) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + description: Payment approved successfully content: application/json: schema: - $ref: '#/components/schemas/SignedRequestChallenge' - examples: - challenge: - summary: Internal account update challenge - value: - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"encoding":"PAYLOAD_ENCODING_HEXADECIMAL","hashFunction":"HASH_FUNCTION_NO_OP","payload":"9f3b...","signWith":"sp1q..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2"}' - requestId: Request:019542f5-b3e7-1d02-0000-000000000010 - expiresAt: '2026-04-08T15:35:00Z' + $ref: '#/components/schemas/IncomingTransaction' '400': - description: Bad request + description: Bad request - Invalid parameters or payment cannot be approved content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending internal account update challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry body does not match the update fields bound into `payloadToSign` on the initial call. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Internal account not found + description: Transaction not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: Conflict - Payment is not in a pending state or has already been processed or timed out. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /internal-accounts/{id}/export: + /transactions/{transactionId}/reject: post: - summary: Export internal account wallet credentials + summary: Reject a pending incoming payment description: | - Export the wallet credentials of an Embedded Wallet internal account. The returned wallet credentials are HPKE-encrypted to the `clientPublicKey` supplied in the request body. - - Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session): - - 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`. - - 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. - - The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against. - operationId: exportInternalAccount + Reject a pending incoming payment that was previously acknowledged with a 202 response. + This endpoint allows platforms to asynchronously reject payments after additional processing. + operationId: rejectPendingPayment tags: - - Internal Accounts + - Transactions security: - BasicAuth: [] parameters: - - name: id + - name: transactionId in: path - description: The id of the internal account to export. + description: Unique identifier of the transaction to reject required: true schema: type: string - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/InternalAccountExportRequest' - examples: - export: - summary: Export request (both steps) - value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + $ref: '#/components/schemas/RejectPaymentRequest' responses: '200': - description: Signed retry accepted. Returns the encrypted wallet credentials. - content: - application/json: - schema: - $ref: '#/components/schemas/InternalAccountExportResponse' - '202': - description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + description: Payment rejected successfully content: application/json: schema: - $ref: '#/components/schemas/SignedRequestChallenge' + $ref: '#/components/schemas/IncomingTransaction' '400': - description: Bad request + description: Bad request - Invalid parameters or payment cannot be rejected content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending export challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Internal account not found + description: Transaction not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: Conflict - Payment is not in a pending state or has already been processed or timed out. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/credentials: + /crypto/estimate-withdrawal-fee: post: - summary: Create an authentication credential + summary: Estimate crypto withdrawal fee description: | - Register an authentication credential for an Embedded Wallet customer. - - Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered. - - Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. - operationId: createAuthCredential + Estimate the network and application fees for a cryptocurrency withdrawal from a crypto internal account to an external blockchain address. Use this to show fee information to customers before they initiate a withdrawal. + operationId: estimateCryptoWithdrawalFee tags: - - Embedded Wallet Auth + - Cross-Currency Transfers security: - BasicAuth: [] - parameters: - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required on the signed retry. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering a credential; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf' + $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeRequest' examples: - emailOtp: - summary: Add an email OTP credential - value: - type: EMAIL_OTP - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - smsOtp: - summary: Add an SMS OTP credential - value: - type: SMS_OTP - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - oauth: - summary: Add an OAuth credential + estimateUSDCWithdrawal: + summary: Estimate fee for USDC withdrawal on Solana value: - type: OAUTH - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q - passkey: - summary: Add a passkey credential - value: - type: PASSKEY - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - nickname: iPhone Face-ID - challenge: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx - attestation: - credentialId: AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY - clientDataJson: eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0 - attestationObject: o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow - transports: - - internal - - hybrid + internalAccountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + currency: USDC + cryptoNetwork: SOLANA + amount: 1000000 + destinationAddress: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU responses: - '201': - description: Authentication credential created successfully. The body is the created `AuthMethod`. For `EMAIL_OTP`, the nickname is the customer email tied to the internal account; for `SMS_OTP`, it is the customer phone number. OTP responses that trigger a secure OTP challenge carry `otpEncryptionTargetBundle` — the HPKE target bundle the client uses to encrypt the OTP attempt on the subsequent `POST /auth/credentials/{id}/verify`. First-time EMAIL_OTP wallet bootstrap responses may omit that bundle; if it is absent, call `POST /auth/credentials/{id}/challenge` for the new credential to issue a fresh OTP and receive `otpEncryptionTargetBundle` before verifying. For `PASSKEY`, the credential must be authenticated for the first time via `POST /auth/credentials/{id}/challenge` followed by `POST /auth/credentials/{id}/verify` to produce a session — there is no inline authentication challenge on the registration response. - content: - application/json: - schema: - $ref: '#/components/schemas/AuthMethodResponse' - examples: - emailOtp: - summary: Email OTP credential created - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: EMAIL_OTP - nickname: example@lightspark.com - otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - smsOtp: - summary: SMS OTP credential created - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: SMS_OTP - nickname: '+14155550123' - otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - oauth: - summary: OAuth credential created - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: OAUTH - nickname: example@lightspark.com - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - passkey: - summary: Passkey credential created - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: PASSKEY - nickname: iPhone Face-ID - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - '202': - description: Challenge issued. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry. + '200': + description: Fee estimation returned successfully content: application/json: schema: - $ref: '#/components/schemas/AuthSignedRequestChallenge' - examples: - emailOtp: - summary: Additional email OTP credential challenge - value: - type: EMAIL_OTP - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userEmail":"jane@example.com","userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_EMAIL"}' - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' - smsOtp: - summary: Additional SMS OTP credential challenge - value: - type: SMS_OTP - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F...","userPhoneNumber":"+14155550123"},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER"}' - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' - oauth: - summary: Additional OAuth credential challenge - value: - type: OAUTH - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"oauthProviders":[{"oidcToken":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...","providerName":"Google"}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS"}' - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' - passkey: - summary: Additional passkey credential challenge - value: - type: PASSKEY - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"authenticators":[{"attestation":{"attestationObject":"o2NmbXRk...","clientDataJson":"eyJjaGFsbGVuZ2UiOiJBcktRa...","credentialId":"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY"},"authenticatorName":"iPhone Face-ID","challenge":"ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx","transports":["internal","hybrid"]}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2"}' - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' + $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeResponse' '400': - description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `SMS_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an SMS OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer. + description: Bad request - Invalid parameters content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration. + description: Unauthorized content: application/json: schema: @@ -4662,62 +4358,24 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - get: - summary: List authentication credentials - description: |- - Retrieve all authentication credentials registered on an Embedded Wallet internal account. - - The response is not paginated: an internal account is expected to have a small, bounded number of credentials (typically 1–5), so all results are returned inline. Additional per-credential detail (such as active session expiry) is available on `GET /auth/sessions`. - operationId: listAuthCredentials + /sandbox/webhooks/test: + post: + summary: Send a test webhook + description: Send a test webhook to the configured endpoint + operationId: sendTestWebhook tags: - - Embedded Wallet Auth + - Sandbox security: - BasicAuth: [] - parameters: - - name: accountId - in: query - description: Internal account id whose authentication credentials to list. - required: true - schema: - type: string - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 responses: '200': - description: Authentication credentials registered on the internal account. Returns an empty `data` array when the internal account has no credentials or when `accountId` does not match any internal account visible to the caller. + description: Webhook delivered successfully content: application/json: schema: - $ref: '#/components/schemas/AuthCredentialListResponse' - examples: - multipleCredentials: - summary: Internal account with multiple authentication credentials - value: - data: - - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: EMAIL_OTP - nickname: example@lightspark.com - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000004 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: OAUTH - nickname: example@lightspark.com - createdAt: '2026-04-08T15:35:00Z' - updatedAt: '2026-04-08T15:35:00Z' - - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000003 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: PASSKEY - credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew - nickname: iPhone Face-ID - createdAt: '2026-04-09T10:15:00Z' - updatedAt: '2026-04-09T10:15:00Z' - empty: - summary: No credentials registered on the account - value: - data: [] + $ref: '#/components/schemas/TestWebhookResponse' '400': - description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier. + description: Bad request - Webhook delivery failed content: application/json: schema: @@ -4734,282 +4392,168 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/credentials/{id}: - delete: - summary: Revoke an authentication credential + /customers/bulk/csv: + post: + summary: Upload customers via CSV file description: | - Revoke an authentication credential on an Embedded Wallet internal account. + Upload a CSV file containing customer information for bulk creation. The CSV file should follow + a specific format with required and optional columns based on customer type. - Revocation is a two-step flow because it must be authorized by a session on a *different* credential on the same internal account: + ### CSV Format + The CSV file should have the following columns: - 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + Required columns for all customers: + - umaAddress: The customer's UMA address (e.g., $john.doe@uma.domain.com) + - platformCustomerId: Your platform's unique identifier for the customer + - customerType: Either "INDIVIDUAL" or "BUSINESS" - 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + Required columns for individual customers: + - fullName: Individual's full name + - birthDate: Date of birth in YYYY-MM-DD format + - addressLine1: Street address line 1 + - city: City + - state: State/Province/Region + - postalCode: Postal/ZIP code + - country: Country code (ISO 3166-1 alpha-2) - The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it. - operationId: revokeAuthCredential + Required columns for business customers: + - businessLegalName: Legal name of the business + - addressLine1: Street address line 1 + - city: City + - state: State/Province/Region + - postalCode: Postal/ZIP code + - country: Country code (ISO 3166-1 alpha-2) + + Optional columns for all customers: + - addressLine2: Street address line 2 + - platformAccountId: Your platform's identifier for the bank account + - description: Optional description for the customer + + Optional columns for individual customers: + - email: Customer's email address + + Optional columns for business customers: + - businessRegistrationNumber: Business registration number + - businessTaxId: Tax identification number + + ### Example CSV + ```csv + umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,platformAccountId,businessLegalName + john.doe@uma.domain.com,customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US + acme@uma.domain.com,biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US + ``` + + The upload process is asynchronous and will return a job ID that can be used to track progress. + You can monitor the job status using the `/customers/bulk/jobs/{jobId}` endpoint. + operationId: uploadCustomersCsv tags: - - Embedded Wallet Auth + - Customers security: - BasicAuth: [] - parameters: - - name: id - in: path - description: The id of the authentication credential to revoke (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). - required: true - schema: - type: string - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: CSV file containing customer information responses: '202': - description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry. - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSignedRequestChallenge' - '204': - description: Authentication credential revoked successfully. - '400': - description: Bad request. Also returned when the target internal account has only a single authentication credential, which cannot be revoked via this endpoint. + description: CSV upload accepted for processing content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/BulkCustomerImportJobAccepted' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this credential, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Authentication credential not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/credentials/{id}/verify: - post: - summary: Verify an authentication credential + /customers/bulk/jobs/{jobId}: + get: + summary: Get bulk import job status description: | - Complete the verification step for a previously created authentication credential and issue a session. - - For `EMAIL_OTP` and `SMS_OTP` credentials, submit the `encryptedOtpBundle` produced by HPKE-encrypting `{otp_code, public_key}` under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued. The server is a pass-through and never sees the plaintext OTP code. On success the response is `202` with a `payloadToSign` carrying the `verificationToken` bound to the client's TEK public key — sign that token with the matching TEK private key, then retry the same request with the full stamp in `Grid-Wallet-Signature` and the `requestId` echoed in `Request-Id`. The signed retry returns `200` with the issued `AuthSession`. The TEK public key becomes the session API key on successful completion. - In sandbox mode, the OTP flow runs real HPKE end-to-end against a sandbox enclave keypair — clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code (`"000000"`) the user "receives" instead of a real email or SMS delivery. - - For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. + Retrieve the current status and results of a bulk customer import job. This endpoint can be used + to track the progress of both CSV uploads. - On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires. - operationId: verifyAuthCredential + The response includes: + - Overall job status + - Progress statistics + - Detailed error information for failed entries + - Completion timestamp when finished + operationId: getBulkCustomerImportJob tags: - - Embedded Wallet Auth + - Customers security: - BasicAuth: [] parameters: - - name: id + - name: jobId in: path - description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + description: ID of the bulk import job to retrieve required: true schema: type: string - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the TEK (Target Encryption Key) keypair the client generated for this login. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification. Not used by `OAUTH` or `PASSKEY` verification, which complete in a single call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response from this endpoint, echoed back exactly here so the server can correlate the signed retry with the issued challenge. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification; must be paired with `Grid-Wallet-Signature`. For `PASSKEY` verification, the `requestId` issued from `POST /auth/credentials/{id}/challenge` is echoed here instead so the server can correlate the assertion with the pending challenge. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf' - examples: - emailOtp: - summary: Verify an email OTP credential (first leg) - value: - type: EMAIL_OTP - encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' - smsOtp: - summary: Verify an SMS OTP credential (first leg) - value: - type: SMS_OTP - encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' - emailOtpSignedRetry: - summary: Signed retry completing an email OTP verification - description: Same request body as the first leg, plus the `Grid-Wallet-Signature` and `Request-Id` headers carrying the stamp over the `verificationToken` and the `requestId` from the prior `202` response. - value: - type: EMAIL_OTP - encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' - oauth: - summary: Verify an OAuth credential - value: - type: OAUTH - oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 - passkey: - summary: Verify a passkey credential - value: - type: PASSKEY - assertion: - credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew - clientDataJson: eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0 - authenticatorData: PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA - signature: MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6jhd45bDx92wjXKs900 responses: '200': - description: Authentication credential verified and session issued - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSession' - '202': - description: Verification challenge issued. Returned only for OTP credentials, on the first leg of the secure OTP login flow. Build an API-key stamp over `payloadToSign` (the `verificationToken`) with the TEK keypair the client generated for this login, then resubmit the same request with that full stamp as `Grid-Wallet-Signature` and `requestId` echoed as `Request-Id` to receive the issued session on the signed retry. - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSignedRequestChallenge' - examples: - emailOtp: - summary: Email OTP verification challenge (sign and retry) - value: - type: EMAIL_OTP - payloadToSign: eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ.eyJzdWIiOiJUWnk2NkVPa1RGYTd2NkpXZ0VxaVgyZGFXOENXc2pMQzVDVU9aRUlGY3hzIiwiaWF0IjoxNzc5NDA3MjIxLCJleHAiOjE3Nzk0MTA4MjF9.gKX9MWYGkw8Y55bgzsgrRftvUHFruIe8yu0w9Kpjp5qnrZnXcTV71WVoltGPsr015IY_oRTOkIFLHmiGNG9zBw - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' - '400': - description: Bad request + description: Job status retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/BulkCustomerImportJob' '401': - description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP` or `SMS_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. For OTP signed retries, returned when `Grid-Wallet-Signature` is missing, malformed, signed by a public key that does not match the one bound into the `verificationToken`, or when `Request-Id` does not match an unexpired pending verification challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Authentication credential not found + description: Job not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when verification attempts for this credential happen too frequently (for example, repeated bad OTPs or rapid-fire reauthentication retries). Clients should back off and retry after the interval indicated by the `Retry-After` response header. - headers: - Retry-After: - description: Number of seconds to wait before retrying the request. - schema: - type: integer - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/credentials/{id}/challenge: + /invitations: post: - summary: Re-issue an authentication credential challenge + summary: Create an UMA invitation description: | - Re-issue the challenge for an existing authentication credential. - - For `EMAIL_OTP` and `SMS_OTP` credentials, this triggers a new one-time password to the contact on file and returns a fresh `otpEncryptionTargetBundle` for the client to HPKE-encrypt the OTP attempt against. After the user receives the new OTP, build the `encryptedOtpBundle` under the new target bundle and call `POST /auth/credentials/{id}/verify` to begin the secure OTP login flow. - - `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`. - - For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. - operationId: challengeAuthCredential + Create an UMA invitation from a given platform customer. + operationId: createInvitation tags: - - Embedded Wallet Auth + - Invitations security: - BasicAuth: [] - parameters: - - name: id - in: path - description: The id of the authentication credential to re-challenge (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). - required: true - schema: - type: string requestBody: - description: Request body. Required when re-challenging a `PASSKEY` credential (must carry `clientPublicKey`). Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient — the OTP is delivered out-of-band. OAuth credentials do not use this endpoint. - required: false + required: true content: application/json: schema: - $ref: '#/components/schemas/AuthCredentialChallengeRequest' - examples: - passkey: - summary: Re-challenge a passkey credential - value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 - emailOtp: - summary: Re-challenge an email-OTP credential (empty body) - value: {} - smsOtp: - summary: Re-challenge an SMS-OTP credential (empty body) - value: {} + $ref: '#/components/schemas/UmaInvitationCreateRequest' responses: - '200': - description: Challenge re-issued for the authentication credential. For `EMAIL_OTP` and `SMS_OTP` the body is a plain `AuthMethod` and a new OTP has been sent. For `PASSKEY` the body is a `PasskeyAuthChallenge` carrying the passkey `credentialId`, freshly issued `challenge`, `requestId`, and `expiresAt` required to complete reauthentication via `POST /auth/credentials/{id}/verify`. + '201': + description: Invitation created successfully content: application/json: schema: - $ref: '#/components/schemas/AuthCredentialResponseOneOf' - examples: - emailOtp: - summary: Email OTP challenge re-issued - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: EMAIL_OTP - nickname: example@lightspark.com - otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:35:00Z' - passkey: - summary: Passkey reauthentication challenge issued - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: PASSKEY - credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew - nickname: iPhone Face-ID - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:35:00Z' - challenge: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729 - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' + $ref: '#/components/schemas/UmaInvitation' '400': description: Bad request content: @@ -5022,120 +4566,91 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Authentication credential not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when challenge re-issues are requested more frequently than the credential challenge rate limit allows. Clients should back off and retry after the interval indicated by the `Retry-After` response header. - headers: - Retry-After: - description: Number of seconds to wait before retrying the request. - schema: - type: integer - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/sessions: + /invitations/{invitationCode}: get: - summary: List active sessions - description: |- - Retrieve all active authentication sessions on an Embedded Wallet internal account. A session is created each time a credential is verified via `POST /auth/credentials/{id}/verify`, and remains active until its `expiresAt` passes or it is revoked via `DELETE /auth/sessions/{id}`. - - The response is not paginated: an internal account is expected to have a small, bounded number of concurrent sessions (one per signed-in device, typically 1–4), so all results are returned inline. - operationId: listAuthSessions + summary: Get an UMA invitation by code + description: | + Retrieve details about an UMA invitation by its invitation code. + operationId: getInvitation tags: - - Embedded Wallet Auth + - Invitations security: - BasicAuth: [] parameters: - - name: accountId - in: query - description: Internal account id whose sessions to list. + - name: invitationCode + in: path + description: The code of the invitation to get required: true schema: type: string - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 responses: '200': - description: Active authentication sessions on the internal account. Returns an empty `data` array when the internal account has no active sessions or when `accountId` does not match any internal account visible to the caller. - content: - application/json: - schema: - $ref: '#/components/schemas/SessionListResponse' - '400': - description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier. + description: Invitation retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/UmaInvitation' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '500': + '404': + description: Invitation not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/sessions/{id}: - delete: - summary: Revoke an authentication session + /invitations/{invitationCode}/claim: + post: + summary: Claim an UMA invitation description: | - Revoke an authentication session on an Embedded Wallet internal account. Revocation is a two-step signed-retry flow: - - 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + Claim an UMA invitation by associating it with an invitee UMA address. - 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + When an invitation is successfully claimed: + 1. The invitation status changes from PENDING to CLAIMED + 2. The invitee UMA address is associated with the invitation + 3. An INVITATION_CLAIMED webhook is triggered to notify the platform that created the invitation - Sessions also expire on their own. `404` is returned whenever the `id` does not match an active session — whether the session was never issued, was already revoked by a prior call, or has expired past its `expiresAt`. The response code reflects the resource state, not an error in the client's flow: re-revoking an already-revoked or expired session is safe and idempotent at the user intent level. - operationId: revokeAuthSession + This endpoint allows customers to accept invitations sent to them by other UMA customers. + operationId: claimInvitation tags: - - Embedded Wallet Auth + - Invitations security: - BasicAuth: [] parameters: - - name: id + - name: invitationCode in: path - description: The id of the session to revoke. + description: The code of the invitation to claim required: true schema: type: string - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UmaInvitationClaimRequest' responses: - '202': - description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry. + '200': + description: Invitation claimed successfully content: application/json: schema: - $ref: '#/components/schemas/AuthSignedRequestChallenge' - '204': - description: Session revoked successfully. + $ref: '#/components/schemas/UmaInvitation' '400': description: Bad request content: @@ -5143,13 +4658,13 @@ paths: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this session, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Session not found + description: Invitation not found content: application/json: schema: @@ -5160,101 +4675,58 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/sessions/{id}/refresh: + /invitations/{invitationCode}/cancel: post: - summary: Refresh an authentication session + summary: Cancel an UMA invitation description: | - Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow: - - 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + Cancel a pending UMA invitation. Only the inviter or platform can cancel an invitation. - 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key. + When an invitation is cancelled: + 1. The invitation status changes from PENDING to CANCELLED + 2. The invitation can no longer be claimed + 3. The invitation URL will show as cancelled when accessed - The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead. - operationId: refreshAuthSession + Only pending invitations can be cancelled. Attempting to cancel an invitation + that is already claimed, expired, or cancelled will result in an error. + operationId: cancelInvitation tags: - - Embedded Wallet Auth + - Invitations security: - BasicAuth: [] parameters: - - name: id + - name: invitationCode in: path - description: The id of the active session to refresh. + description: The code of the invitation to cancel required: true schema: type: string - example: Session:019542f5-b3e7-1d02-0000-000000000003 - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the current session API keypair. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in the prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:019542f5-b3e7-1d02-0000-000000000010 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSessionRefreshRequest' - examples: - refresh: - summary: Refresh an active session - value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: - '201': - description: New authentication session created successfully. - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSession' - examples: - session: - summary: Refreshed authentication session - value: - id: Session:019542f5-b3e7-1d02-0000-000000000011 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: EMAIL_OTP - encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf - nickname: example@lightspark.com - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:35:00Z' - expiresAt: '2026-04-08T15:50:00Z' - '202': - description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the current session API keypair, then echo `requestId` on the signed retry. + '200': + description: Invitation cancelled successfully content: application/json: schema: - $ref: '#/components/schemas/SignedRequestChallenge' - examples: - challenge: - summary: Session refresh challenge - value: - payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' - requestId: Request:019542f5-b3e7-1d02-0000-000000000010 - expiresAt: '2026-04-08T15:35:00Z' + $ref: '#/components/schemas/UmaInvitation' '400': - description: Bad request + description: Bad request - Invitation cannot be cancelled (already claimed, expired, or cancelled) content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the `BasicAuth` credentials are missing or invalid, when the target session is no longer active and cannot be used for refresh, when the signed retry omits `Grid-Wallet-Signature`, when the provided signature is malformed or does not match the pending refresh challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - Only the platform which created the invitation can cancel it + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' '404': - description: Session not found + description: Invitation not found content: application/json: schema: @@ -5265,80 +4737,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/delegated-keys: + /sandbox/send: post: - summary: Create a delegated signing key + summary: Simulate sending funds description: | - Delegate Spark token-transaction signing authority for a card funding source backed by an Embedded Wallet internal account to a Grid-custodied P-256 API key. Grid uses the requested card and internal account to identify the wallet funding source, generates the keypair server-side, creates an isolated signer identity holding the public key, then policies granting that identity signing and self-revocation authority. The private key is custodied by Grid and never returned. Both activities must be authorized by the wallet owner, so creation is a three-leg signed-retry flow: - - 1. Call `POST /auth/delegated-keys` with no signature headers. Grid generates the delegated keypair and the response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - - 2. Use the session API keypair of a verified credential on the requested Embedded Wallet internal account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The response is a second `202` with a new `payloadToSign`, `requestId`, and `expiresAt`. - - 3. Stamp the new `payloadToSign` with the same session keypair and retry once more with the new `Request-Id`. The signed retry returns `201` with the created `DelegatedKey` in `ACTIVE` status. - - The same request body must be sent on all three legs. A flow abandoned after the second leg leaves the key in `PENDING` status: the signer identity exists but holds no policies, so it cannot sign or revoke itself. Abandoned `PENDING` keys do not block creating another delegated key. After activation, Grid uses the custodied key to authorize signing for the card's Embedded Wallet funding account in place of a session keypair; the platform never handles the key material. - - Each card funding source may have at most one `ACTIVE` delegated key for its Embedded Wallet funding account; revoke the existing active key before creating a new one. A delegated key authorizes raw-payload signing for the wallet and cannot be scoped to amounts or recipients by the public API. Revoke it with `DELETE /auth/delegated-keys/{id}` when no longer needed. - operationId: createDelegatedKey + Simulate sending funds to the bank account as instructed in the quote. + This endpoint is only for the sandbox environment and will fail for production platforms/keys. + operationId: sandboxSend tags: - - Embedded Wallet Auth + - Sandbox security: - BasicAuth: [] - parameters: - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified credential on the same internal account. Required on the signed retries; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in the prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retries; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/DelegatedKeyCreateRequest' - examples: - create: - summary: Delegate signing to a Grid-custodied key - value: - cardId: Card:019542f5-b3e7-1d02-0000-000000000010 - internalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - nickname: Card payments key + $ref: '#/components/schemas/SandboxSendRequest' responses: - '201': - description: Delegated key created and policy granted. The key is `ACTIVE` and Grid may use it to stamp card-payment quote executions for this card funding source's Embedded Wallet funding account. - content: - application/json: - schema: - $ref: '#/components/schemas/DelegatedKey' - examples: - delegatedKey: - summary: Active delegated key - value: - id: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 - cardId: Card:019542f5-b3e7-1d02-0000-000000000010 - fundingSourceId: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - publicKey: 02a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90 - nickname: Card payments key - status: ACTIVE - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:42Z' - '202': - description: Challenge issued for the next leg. Stamp `payloadToSign` and retry the same request with `Grid-Wallet-Signature` and `Request-Id`. + '200': + description: Funds received successfully content: application/json: schema: - $ref: '#/components/schemas/DelegatedKeySignedRequestChallenge' + $ref: '#/components/schemas/OutgoingTransaction' '400': description: Bad request content: @@ -5346,59 +4768,53 @@ paths: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing on a retry, malformed, or does not match the pending challenge, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Card, card funding source, or Embedded Wallet funding account not found + '403': + description: Forbidden - request was made with a production platform token content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: An `ACTIVE` delegated key already exists for this card funding source. Revoke it with `DELETE /auth/delegated-keys/{id}` before creating a new one. + $ref: '#/components/schemas/Error403' + '404': + description: Quote not found content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - get: - summary: List delegated signing keys - description: List delegated signing keys for an Embedded Wallet internal account, a card funding source, or both, including `PENDING` keys (user created but policy leg never completed) and `REVOKED` keys. At least one of `accountId` or `fundingSourceId` must be supplied. - operationId: listDelegatedKeys + /sandbox/uma/receive: + post: + summary: Simulate payment send to test receiving an UMA payment + description: | + Simulate sending payment from an sandbox uma address to a platform customer to test payment receive. + This endpoint is only for the sandbox environment and will fail for production platforms/keys. + operationId: sandboxReceive tags: - - Embedded Wallet Auth + - Sandbox security: - BasicAuth: [] - parameters: - - name: accountId - in: query - required: false - description: The id of the internal account whose delegated keys to list. - schema: - type: string - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - - name: fundingSourceId - in: query - required: false - description: The id of the card funding source whose delegated keys to list. - schema: - type: string - example: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxUmaReceiveRequest' responses: '200': - description: Delegated keys matching the supplied filters. Returns an empty `data` array when no matching delegated keys are visible to the caller. + description: Payment triggered successfully content: application/json: schema: - $ref: '#/components/schemas/DelegatedKeyListResponse' + $ref: '#/components/schemas/IncomingTransaction' '400': description: Bad request content: @@ -5411,44 +4827,85 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Sender or receiver not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/delegated-keys/{id}: - get: - summary: Get a delegated signing key - description: Retrieve a delegated signing key by its system-generated id. - operationId: getDelegatedKeyById + /sandbox/internal-accounts/{accountId}/fund: + post: + summary: Simulate funding an internal account + description: | + Simulate receiving funds into an internal account in the sandbox environment. This is useful for testing scenarios where you need to add funds to a customer's or platform's internal account without going through a real bank transfer or following payment instructions. + This endpoint is only for the sandbox environment and will fail for production platforms/keys. + operationId: sandboxFundInternalAccount tags: - - Embedded Wallet Auth + - Sandbox security: - BasicAuth: [] parameters: - - name: id + - name: accountId in: path - description: The id of the delegated key to retrieve (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys` or `GET /auth/delegated-keys`). required: true + description: The ID of the internal account to fund schema: type: string - example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 + example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxFundRequest' + examples: + fundUSDAccount: + summary: Fund USD internal account with $1,000 + value: + amount: 100000 + fundBTCAccount: + summary: Fund BTC internal account with 0.01 BTC + value: + amount: 1000000 responses: '200': - description: Successful operation + description: Internal account funded successfully content: application/json: schema: - $ref: '#/components/schemas/DelegatedKey' + $ref: '#/components/schemas/InternalAccount' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' '404': - description: Delegated key not found + description: Internal account not found content: application/json: schema: @@ -5459,60 +4916,88 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - delete: - summary: Revoke a delegated signing key + /uma-providers: + get: + summary: List available Counterparty Providers description: | - Revoke an `ACTIVE` delegated signing key. Grid uses the custodied delegated key to authorize deleting its own signer identity. Deleting the identity also removes its API key, after which the delegated key can no longer sign. The response is `204` when revocation completes. - - The underlying signing policies are left in place. Their consensus references the now-deleted signer identity, so they can never authorize anything, and deleting them is unnecessary for correctness or security. - operationId: revokeDelegatedKey + Retrieve a list of available Counterparty Providers. The response includes basic information about each provider, such as its UMA address, name, and supported currencies. + operationId: getAvailableUmaProviders tags: - - Embedded Wallet Auth - security: - - BasicAuth: [] + - Available UMA Providers parameters: - - name: id - in: path - description: The id of the delegated key to revoke (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys`). - required: true + - in: query + name: countryCode + description: The alpha-2 representation of a country, as defined by the ISO 3166-1 standard. + required: false schema: type: string - example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 + example: US + - in: query + name: currencyCode + description: The ISO 4217 currency code to filter providers by supported currency. + required: false + schema: + type: string + example: USD + - in: query + name: hasBlockedProviders + description: Whether to include providers which are not on your allowlist in the response. By default the response will include blocked providers. + required: false + schema: + type: boolean + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc + security: + - BasicAuth: [] responses: - '204': - description: Delegated key revoked. The key can no longer authorize signing. - '400': - description: Bad request. Returned when the delegated key has already been revoked or is not `ACTIVE`. + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/UmaProviderListResponse' '401': - description: Unauthorized. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Delegated key not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents: + /tokens: post: - summary: Create an agent - description: | - Create a new agent with a specified policy. Returns the created agent and a device code that must be redeemed by the agent software to complete installation. - operationId: createAgent + summary: Create a new API token + description: Create a new API token to access the Grid APIs. + operationId: createToken tags: - - Agent Management + - API Tokens security: - BasicAuth: [] requestBody: @@ -5520,14 +5005,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AgentCreateRequest' + $ref: '#/components/schemas/ApiTokenCreateRequest' responses: '201': - description: Agent created successfully + description: API token created successfully content: application/json: schema: - $ref: '#/components/schemas/AgentCreateResponse' + $ref: '#/components/schemas/ApiToken' '400': description: Bad request content: @@ -5547,56 +5032,46 @@ paths: schema: $ref: '#/components/schemas/Error500' get: - summary: List agents - description: Retrieve a paginated list of agents for the authenticated platform. - operationId: listAgents + summary: List tokens + description: | + Retrieve a list of API tokens with optional filtering parameters. Returns all tokens that match + the specified filters. If no filters are provided, returns all tokens (paginated). + operationId: listTokens tags: - - Agent Management + - API Tokens security: - BasicAuth: [] parameters: - - name: customerId + - name: name in: query - description: Filter by customer ID + description: Filter by name of the token required: false schema: type: string - - name: isPaused - in: query - description: Filter by paused status - required: false - schema: - type: boolean - - name: isConnected - in: query - description: Filter by connection status (whether the device code has been redeemed) - required: false - schema: - type: boolean - name: createdAfter in: query - description: Filter agents created after this timestamp (inclusive) + description: Filter customers created after this timestamp (inclusive) required: false schema: type: string format: date-time - name: createdBefore in: query - description: Filter agents created before this timestamp (inclusive) + description: Filter customers created before this timestamp (inclusive) required: false schema: type: string format: date-time - name: updatedAfter in: query - description: Filter agents updated after this timestamp (inclusive) + description: Filter customers updated after this timestamp (inclusive) required: false schema: type: string format: date-time - name: updatedBefore in: query - description: Filter agents updated before this timestamp (inclusive) + description: Filter customers updated before this timestamp (inclusive) required: false schema: type: string @@ -5622,7 +5097,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AgentListResponse' + $ref: '#/components/schemas/TokenListResponse' '400': description: Bad request - Invalid parameters content: @@ -5641,153 +5116,192 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/approvals: + /tokens/{tokenId}: + parameters: + - name: tokenId + in: path + description: System-generated unique token identifier + required: true + schema: + type: string get: - summary: List agent transaction approval requests - description: | - Retrieve a paginated list of agent actions that require platform approval. Filter by `agentId` or `customerId` to scope results to a specific agent or customer. Approve or reject individual actions via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`. - operationId: listAgentApprovals + summary: Get API token by ID + description: Retrieve an API token by their system-generated ID + operationId: getTokenById tags: - - Agent Management + - API Tokens security: - BasicAuth: [] - parameters: - - name: agentId - in: query - description: Filter by agent ID - required: false - schema: - type: string - - name: customerId - in: query - description: Filter by customer ID - required: false - schema: - type: string - - name: startDate - in: query - description: Filter by start date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: endDate - in: query - description: Filter by end date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string - - name: sortOrder - in: query - description: Order to sort results in - required: false - schema: - type: string - enum: - - asc - - desc - default: desc responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/AgentActionListResponse' - '400': - description: Bad request - Invalid parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/ApiToken' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Token not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me: - get: - summary: Get current agent - description: | - Retrieve the authenticated agent's own profile, policy, and current usage. This endpoint is called by the agent software itself using its own credentials (obtained via device code redemption) rather than platform credentials. - operationId: getAgentMe + delete: + summary: Delete API token by ID + description: Delete an API token by their system-generated ID + operationId: deleteTokenById tags: - - Agent Operations + - API Tokens security: - - AgentAuth: [] + - BasicAuth: [] responses: - '200': - description: Successful operation + '204': + description: API token deleted successfully + '400': + description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Agent' + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Token not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/{agentId}: + /internal-accounts/{id}: parameters: - - name: agentId + - name: id in: path - description: System-generated unique agent identifier + description: The id of the internal account to update. required: true schema: type: string - get: - summary: Get agent by ID - description: Retrieve an agent by its system-generated ID. - operationId: getAgentById + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + patch: + summary: Update internal account + description: | + Update mutable fields on an internal account. Today this supports updating the wallet privacy setting for an Embedded Wallet internal account. + + Updating wallet privacy is a two-step signed-retry flow: + + 1. Call `PATCH /internal-accounts/{id}` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same update fields submitted in step 1. The signed retry returns `200` with the updated internal account. + operationId: updateInternalAccount tags: - - Agent Management + - Internal Accounts security: - BasicAuth: [] + parameters: + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountUpdateRequest' + examples: + updateWalletPrivacy: + summary: Update wallet privacy request (both steps) + value: + privateEnabled: true responses: '200': - description: Successful operation + description: Signed retry accepted. Returns the updated internal account. content: application/json: schema: - $ref: '#/components/schemas/Agent' + $ref: '#/components/schemas/InternalAccount' + examples: + enabled: + summary: Wallet privacy enabled + value: + id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: EMBEDDED_WALLET + status: ACTIVE + balance: + amount: 12550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + totalBalance: + amount: 12550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + fundingPaymentInstructions: [] + privateEnabled: true + createdAt: '2026-04-08T15:30:00Z' + updatedAt: '2026-04-08T15:35:02Z' + '202': + description: Challenge issued. The response contains `payloadToSign` (which binds the submitted update fields) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/SignedRequestChallenge' + examples: + challenge: + summary: Internal account update challenge + value: + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"encoding":"PAYLOAD_ENCODING_HEXADECIMAL","hashFunction":"HASH_FUNCTION_NO_OP","payload":"9f3b...","signWith":"sp1q..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2"}' + requestId: Request:019542f5-b3e7-1d02-0000-000000000010 + expiresAt: '2026-04-08T15:35:00Z' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending internal account update challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry body does not match the update fields bound into `payloadToSign` on the initial call. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent not found + description: Internal account not found content: application/json: schema: @@ -5798,27 +5312,69 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - patch: - summary: Update agent - description: Update an agent's name or paused state. - operationId: updateAgent + /internal-accounts/{id}/export: + post: + summary: Export internal account wallet credentials + description: | + Export the wallet credentials of an Embedded Wallet internal account. The returned wallet credentials are HPKE-encrypted to the `clientPublicKey` supplied in the request body. + + Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session): + + 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. + + The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against. + operationId: exportInternalAccount tags: - - Agent Management + - Internal Accounts security: - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the internal account to export. + required: true + schema: + type: string + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/AgentUpdateRequest' + $ref: '#/components/schemas/InternalAccountExportRequest' + examples: + export: + summary: Export request (both steps) + value: + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '200': - description: Agent updated successfully + description: Signed retry accepted. Returns the encrypted wallet credentials. content: application/json: schema: - $ref: '#/components/schemas/Agent' + $ref: '#/components/schemas/InternalAccountExportResponse' + '202': + description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/SignedRequestChallenge' '400': description: Bad request content: @@ -5826,13 +5382,13 @@ paths: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending export challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent not found + description: Internal account not found content: application/json: schema: @@ -5843,79 +5399,167 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - delete: - summary: Delete agent - description: Permanently delete an agent. Connected agent software will lose access immediately. - operationId: deleteAgent - tags: - - Agent Management - security: - - BasicAuth: [] - responses: - '204': - description: Agent deleted successfully - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Agent not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /agents/{agentId}/policy: - parameters: - - name: agentId - in: path - description: System-generated unique agent identifier - required: true - schema: - type: string - patch: - summary: Update agent policy + /auth/credentials: + post: + summary: Create an authentication credential description: | - Partially update an agent's policy. Only provided fields will be updated; omitted fields retain their current values. Policy changes take effect immediately. - operationId: updateAgentPolicy + Register an authentication credential for an Embedded Wallet customer. + + Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered. + + Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. + operationId: createAuthCredential tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required on the signed retry. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering a credential; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/AgentPolicyUpdateRequest' + $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf' + examples: + emailOtp: + summary: Add an email OTP credential + value: + type: EMAIL_OTP + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + smsOtp: + summary: Add an SMS OTP credential + value: + type: SMS_OTP + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + oauth: + summary: Add an OAuth credential + value: + type: OAUTH + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q + passkey: + summary: Add a passkey credential + value: + type: PASSKEY + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + nickname: iPhone Face-ID + challenge: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx + attestation: + credentialId: AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY + clientDataJson: eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0 + attestationObject: o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow + transports: + - internal + - hybrid responses: - '200': - description: Agent policy updated successfully + '201': + description: Authentication credential created successfully. The body is the created `AuthMethod`. For `EMAIL_OTP`, the nickname is the customer email tied to the internal account; for `SMS_OTP`, it is the customer phone number. OTP responses that trigger a secure OTP challenge carry `otpEncryptionTargetBundle` — the HPKE target bundle the client uses to encrypt the OTP attempt on the subsequent `POST /auth/credentials/{id}/verify`. First-time EMAIL_OTP wallet bootstrap responses may omit that bundle; if it is absent, call `POST /auth/credentials/{id}/challenge` for the new credential to issue a fresh OTP and receive `otpEncryptionTargetBundle` before verifying. For `PASSKEY`, the credential must be authenticated for the first time via `POST /auth/credentials/{id}/challenge` followed by `POST /auth/credentials/{id}/verify` to produce a session — there is no inline authentication challenge on the registration response. content: application/json: schema: - $ref: '#/components/schemas/Agent' + $ref: '#/components/schemas/AuthMethodResponse' + examples: + emailOtp: + summary: Email OTP credential created + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: EMAIL_OTP + nickname: example@lightspark.com + otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + smsOtp: + summary: SMS OTP credential created + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: SMS_OTP + nickname: '+14155550123' + otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + oauth: + summary: OAuth credential created + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: OAUTH + nickname: example@lightspark.com + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + passkey: + summary: Passkey credential created + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: PASSKEY + nickname: iPhone Face-ID + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + '202': + description: Challenge issued. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthSignedRequestChallenge' + examples: + emailOtp: + summary: Additional email OTP credential challenge + value: + type: EMAIL_OTP + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userEmail":"jane@example.com","userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_EMAIL"}' + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' + smsOtp: + summary: Additional SMS OTP credential challenge + value: + type: SMS_OTP + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F...","userPhoneNumber":"+14155550123"},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER"}' + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' + oauth: + summary: Additional OAuth credential challenge + value: + type: OAUTH + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"oauthProviders":[{"oidcToken":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...","providerName":"Google"}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS"}' + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' + passkey: + summary: Additional passkey credential challenge + value: + type: PASSKEY + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"authenticators":[{"attestation":{"attestationObject":"o2NmbXRk...","clientDataJson":"eyJjaGFsbGVuZ2UiOiJBcktRa...","credentialId":"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY"},"authenticatorName":"iPhone Face-ID","challenge":"ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx","transports":["internal","hybrid"]}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2"}' + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' '400': - description: Bad request + description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `SMS_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an SMS OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer. content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent not found + description: Internal account not found content: application/json: schema: @@ -5926,32 +5570,62 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/{agentId}/device-codes: - parameters: - - name: agentId - in: path - description: System-generated unique agent identifier - required: true - schema: - type: string - post: - summary: Regenerate a device code - description: | - Generate a new device code for an existing agent. Use this when the original device code has expired before being redeemed, or when the agent software needs to be reinstalled. Any previously issued unredeemed device codes for this agent are invalidated. - operationId: regenerateAgentDeviceCode + get: + summary: List authentication credentials + description: |- + Retrieve all authentication credentials registered on an Embedded Wallet internal account. + + The response is not paginated: an internal account is expected to have a small, bounded number of credentials (typically 1–5), so all results are returned inline. Additional per-credential detail (such as active session expiry) is available on `GET /auth/sessions`. + operationId: listAuthCredentials tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: accountId + in: query + description: Internal account id whose authentication credentials to list. + required: true + schema: + type: string + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 responses: - '201': - description: New device code generated successfully + '200': + description: Authentication credentials registered on the internal account. Returns an empty `data` array when the internal account has no credentials or when `accountId` does not match any internal account visible to the caller. content: application/json: schema: - $ref: '#/components/schemas/AgentDeviceCode' + $ref: '#/components/schemas/AuthCredentialListResponse' + examples: + multipleCredentials: + summary: Internal account with multiple authentication credentials + value: + data: + - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: EMAIL_OTP + nickname: example@lightspark.com + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000004 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: OAUTH + nickname: example@lightspark.com + createdAt: '2026-04-08T15:35:00Z' + updatedAt: '2026-04-08T15:35:00Z' + - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000003 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: PASSKEY + credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew + nickname: iPhone Face-ID + createdAt: '2026-04-09T10:15:00Z' + updatedAt: '2026-04-09T10:15:00Z' + empty: + summary: No credentials registered on the account + value: + data: [] '400': - description: Bad request + description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier. content: application/json: schema: @@ -5962,332 +5636,352 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Agent not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Agent already has an active connection and cannot regenerate a device code - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/{agentId}/actions/{actionId}/approve: - parameters: - - name: agentId - in: path - description: System-generated unique agent identifier - required: true - schema: - type: string - - name: actionId - in: path - description: Unique identifier of the agent action to approve - required: true - schema: - type: string - post: - summary: Approve an agent action + /auth/credentials/{id}: + delete: + summary: Revoke an authentication credential description: | - Approve a pending agent action, allowing Grid to proceed with execution. The action must have status `PENDING_APPROVAL`. Once approved, Grid executes the underlying operation (quote execution or transfer) and the action transitions to `APPROVED`. - For `EXECUTE_QUOTE` actions, note that the underlying quote may have expired between submission and approval — in that case the action will transition to `FAILED` instead. - This endpoint is called by the platform's backend using platform credentials, not by the agent itself. - operationId: approveAgentAction + Revoke an authentication credential on an Embedded Wallet internal account. + + Revocation is a two-step flow because it must be authorized by a session on a *different* credential on the same internal account: + + 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + + The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it. + operationId: revokeAuthCredential tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the authentication credential to revoke (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + required: true + schema: + type: string + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 responses: - '200': - description: Action approved successfully. Returns the updated AgentAction. + '202': + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry. content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/AuthSignedRequestChallenge' + '204': + description: Authentication credential revoked successfully. '400': - description: Bad request - Action cannot be approved + description: Bad request. Also returned when the target internal account has only a single authentication credential, which cannot be revoked via this endpoint. content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this credential, or when the `Request-Id` does not match an unexpired pending challenge. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent or action not found + description: Authentication credential not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Action is not pending approval or has already been processed - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/{agentId}/actions/{actionId}/reject: - parameters: - - name: agentId - in: path - description: System-generated unique agent identifier - required: true - schema: - type: string - - name: actionId - in: path - description: Unique identifier of the agent action to reject - required: true - schema: - type: string + /auth/credentials/{id}/verify: post: - summary: Reject an agent action + summary: Verify an authentication credential description: | - Reject a pending agent action, preventing execution. The action must have status `PENDING_APPROVAL`. Once rejected, the action transitions to `REJECTED` and the underlying operation is not executed. - This endpoint is called by the platform's backend using platform credentials, not by the agent itself. - operationId: rejectAgentAction + Complete the verification step for a previously created authentication credential and issue a session. + + For `EMAIL_OTP` and `SMS_OTP` credentials, submit the `encryptedOtpBundle` produced by HPKE-encrypting `{otp_code, public_key}` under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued. The server is a pass-through and never sees the plaintext OTP code. On success the response is `202` with a `payloadToSign` carrying the `verificationToken` bound to the client's TEK public key — sign that token with the matching TEK private key, then retry the same request with the full stamp in `Grid-Wallet-Signature` and the `requestId` echoed in `Request-Id`. The signed retry returns `200` with the issued `AuthSession`. The TEK public key becomes the session API key on successful completion. + In sandbox mode, the OTP flow runs real HPKE end-to-end against a sandbox enclave keypair — clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code (`"000000"`) the user "receives" instead of a real email or SMS delivery. + + For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. + + On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires. + operationId: verifyAuthCredential tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + required: true + schema: + type: string + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the TEK (Target Encryption Key) keypair the client generated for this login. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification. Not used by `OAUTH` or `PASSKEY` verification, which complete in a single call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response from this endpoint, echoed back exactly here so the server can correlate the signed retry with the issued challenge. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification; must be paired with `Grid-Wallet-Signature`. For `PASSKEY` verification, the `requestId` issued from `POST /auth/credentials/{id}/challenge` is echoed here instead so the server can correlate the assertion with the pending challenge. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: - required: false + required: true content: application/json: schema: - $ref: '#/components/schemas/AgentActionRejectRequest' + $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf' + examples: + emailOtp: + summary: Verify an email OTP credential (first leg) + value: + type: EMAIL_OTP + encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' + smsOtp: + summary: Verify an SMS OTP credential (first leg) + value: + type: SMS_OTP + encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' + emailOtpSignedRetry: + summary: Signed retry completing an email OTP verification + description: Same request body as the first leg, plus the `Grid-Wallet-Signature` and `Request-Id` headers carrying the stamp over the `verificationToken` and the `requestId` from the prior `202` response. + value: + type: EMAIL_OTP + encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' + oauth: + summary: Verify an OAuth credential + value: + type: OAUTH + oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + passkey: + summary: Verify a passkey credential + value: + type: PASSKEY + assertion: + credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew + clientDataJson: eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0 + authenticatorData: PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA + signature: MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6jhd45bDx92wjXKs900 responses: '200': - description: Action rejected successfully. Returns the updated AgentAction. + description: Authentication credential verified and session issued content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/AuthSession' + '202': + description: Verification challenge issued. Returned only for OTP credentials, on the first leg of the secure OTP login flow. Build an API-key stamp over `payloadToSign` (the `verificationToken`) with the TEK keypair the client generated for this login, then resubmit the same request with that full stamp as `Grid-Wallet-Signature` and `requestId` echoed as `Request-Id` to receive the issued session on the signed retry. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthSignedRequestChallenge' + examples: + emailOtp: + summary: Email OTP verification challenge (sign and retry) + value: + type: EMAIL_OTP + payloadToSign: eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ.eyJzdWIiOiJUWnk2NkVPa1RGYTd2NkpXZ0VxaVgyZGFXOENXc2pMQzVDVU9aRUlGY3hzIiwiaWF0IjoxNzc5NDA3MjIxLCJleHAiOjE3Nzk0MTA4MjF9.gKX9MWYGkw8Y55bgzsgrRftvUHFruIe8yu0w9Kpjp5qnrZnXcTV71WVoltGPsr015IY_oRTOkIFLHmiGNG9zBw + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' '400': - description: Bad request - Action cannot be rejected + description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP` or `SMS_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. For OTP signed retries, returned when `Grid-Wallet-Signature` is missing, malformed, signed by a public key that does not match the one bound into the `verificationToken`, or when `Request-Id` does not match an unexpired pending verification challenge. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent or action not found + description: Authentication credential not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Action is not pending approval or has already been processed + '429': + description: Too many requests. Returned with `RATE_LIMITED` when verification attempts for this credential happen too frequently (for example, repeated bad OTPs or rapid-fire reauthentication retries). Clients should back off and retry after the interval indicated by the `Retry-After` response header. + headers: + Retry-After: + description: Number of seconds to wait before retrying the request. + schema: + type: integer content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/device-codes/{code}/status: - parameters: - - name: code - in: path - description: The device code to check - required: true - schema: - type: string - get: - summary: Get device code status + /auth/credentials/{id}/challenge: + post: + summary: Re-issue an authentication credential challenge description: | - Check whether a device code has been redeemed. Use this to poll for agent installation completion after creating an agent. - operationId: getAgentDeviceCodeStatus + Re-issue the challenge for an existing authentication credential. + + For `EMAIL_OTP` and `SMS_OTP` credentials, this triggers a new one-time password to the contact on file and returns a fresh `otpEncryptionTargetBundle` for the client to HPKE-encrypt the OTP attempt against. After the user receives the new OTP, build the `encryptedOtpBundle` under the new target bundle and call `POST /auth/credentials/{id}/verify` to begin the secure OTP login flow. + + `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`. + + For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. + operationId: challengeAuthCredential tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the authentication credential to re-challenge (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + required: true + schema: + type: string + requestBody: + description: Request body. Required when re-challenging a `PASSKEY` credential (must carry `clientPublicKey`). Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient — the OTP is delivered out-of-band. OAuth credentials do not use this endpoint. + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/AuthCredentialChallengeRequest' + examples: + passkey: + summary: Re-challenge a passkey credential + value: + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + emailOtp: + summary: Re-challenge an email-OTP credential (empty body) + value: {} + smsOtp: + summary: Re-challenge an SMS-OTP credential (empty body) + value: {} responses: '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/AgentDeviceCodeStatusResponse' - '401': - description: Unauthorized + description: Challenge re-issued for the authentication credential. For `EMAIL_OTP` and `SMS_OTP` the body is a plain `AuthMethod` and a new OTP has been sent. For `PASSKEY` the body is a `PasskeyAuthChallenge` carrying the passkey `credentialId`, freshly issued `challenge`, `requestId`, and `expiresAt` required to complete reauthentication via `POST /auth/credentials/{id}/verify`. content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Device code not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error + $ref: '#/components/schemas/AuthCredentialResponseOneOf' + examples: + emailOtp: + summary: Email OTP challenge re-issued + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: EMAIL_OTP + nickname: example@lightspark.com + otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:35:00Z' + passkey: + summary: Passkey reauthentication challenge issued + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: PASSKEY + credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew + nickname: iPhone Face-ID + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:35:00Z' + challenge: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729 + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' + '400': + description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Error500' - /agents/device-codes/{code}/redeem: - parameters: - - name: code - in: path - description: The device code to redeem - required: true - schema: - type: string - post: - summary: Redeem device code - description: | - Redeem a device code to obtain agent credentials. This endpoint is called by the agent software during installation. On success, returns a Bearer access token that the agent uses for all subsequent API calls. The token is returned only once and must be stored securely. - This endpoint does not require platform authentication — the device code itself serves as proof of authorization. - operationId: redeemAgentDeviceCode - tags: - - Agent Management - security: [] - responses: - '200': - description: Device code redeemed successfully + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse' - '400': - description: Bad request (e.g., code already redeemed or expired) + $ref: '#/components/schemas/Error401' + '404': + description: Authentication credential not found content: application/json: schema: - $ref: '#/components/schemas/Error400' - '404': - description: Device code not found + $ref: '#/components/schemas/Error404' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when challenge re-issues are requested more frequently than the credential challenge rate limit allows. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + headers: + Retry-After: + description: Number of seconds to wait before retrying the request. + schema: + type: integer content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/transactions: + /auth/sessions: get: - summary: List agent transactions - description: | - Retrieve a paginated list of transactions for the authenticated agent's customer. Results are automatically scoped to the agent's associated customer — no customer filter is needed or accepted. - operationId: agentListTransactions + summary: List active sessions + description: |- + Retrieve all active authentication sessions on an Embedded Wallet internal account. A session is created each time a credential is verified via `POST /auth/credentials/{id}/verify`, and remains active until its `expiresAt` passes or it is revoked via `DELETE /auth/sessions/{id}`. + + The response is not paginated: an internal account is expected to have a small, bounded number of concurrent sessions (one per signed-in device, typically 1–4), so all results are returned inline. + operationId: listAuthSessions tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: accountIdentifier - in: query - description: Filter by account identifier (matches either sender or receiver) - required: false - schema: - type: string - - name: senderAccountIdentifier - in: query - description: Filter by sender account identifier - required: false - schema: - type: string - - name: receiverAccountIdentifier - in: query - description: Filter by receiver account identifier - required: false - schema: - type: string - - name: status - in: query - description: Filter by transaction status - required: false - schema: - $ref: '#/components/schemas/TransactionStatus' - - name: type - in: query - description: Filter by transaction type - required: false - schema: - $ref: '#/components/schemas/TransactionType' - - name: reference - in: query - description: Filter by reference - required: false - schema: - type: string - - name: startDate - in: query - description: Filter by start date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: endDate - in: query - description: Filter by end date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string - - name: sortOrder + - name: accountId in: query - description: Order to sort results in - required: false + description: Internal account id whose sessions to list. + required: true schema: type: string - enum: - - asc - - desc - default: desc + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 responses: '200': - description: Successful operation + description: Active authentication sessions on the internal account. Returns an empty `data` array when the internal account has no active sessions or when `accountId` does not match any internal account visible to the caller. content: application/json: schema: - $ref: '#/components/schemas/TransactionListResponse' + $ref: '#/components/schemas/SessionListResponse' '400': - description: Bad request - Invalid parameters + description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier. content: application/json: schema: @@ -6304,38 +5998,66 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/transactions/{transactionId}: - parameters: - - name: transactionId - in: path - description: Unique identifier of the transaction - required: true - schema: - type: string - get: - summary: Get agent transaction by ID + /auth/sessions/{id}: + delete: + summary: Revoke an authentication session description: | - Retrieve a specific transaction belonging to the authenticated agent's customer. Returns 404 if the transaction exists but belongs to a different customer. - operationId: agentGetTransaction + Revoke an authentication session on an Embedded Wallet internal account. Revocation is a two-step signed-retry flow: + + 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + + Sessions also expire on their own. `404` is returned whenever the `id` does not match an active session — whether the session was never issued, was already revoked by a prior call, or has expired past its `expiresAt`. The response code reflects the resource state, not an error in the client's flow: re-revoking an already-revoked or expired session is safe and idempotent at the user intent level. + operationId: revokeAuthSession tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the session to revoke. + required: true + schema: + type: string + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 responses: - '200': - description: Successful operation + '202': + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry. content: application/json: schema: - $ref: '#/components/schemas/TransactionOneOf' + $ref: '#/components/schemas/AuthSignedRequestChallenge' + '204': + description: Session revoked successfully. + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this session, or when the `Request-Id` does not match an unexpired pending challenge. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Session not found content: application/json: schema: @@ -6346,107 +6068,101 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/quotes: + /auth/sessions/{id}/refresh: post: - summary: Create a transfer quote + summary: Refresh an authentication session description: | - Generate a quote for a cross-currency transfer on behalf of the authenticated agent's customer. Accounts referenced in the request must belong to the agent's customer. Requires the CREATE_QUOTES permission in the agent's policy. - If the agent's defaultExecutionMode is APPROVAL_REQUIRED, or the quote amount exceeds the agent's approvalThresholds, the resulting transaction will require explicit approval before funds move. - operationId: agentCreateQuote + Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow: + + 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key. + + The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead. + operationId: refreshAuthSession tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: Idempotency-Key + - name: id + in: path + description: The id of the active session to refresh. + required: true + schema: + type: string + example: Session:019542f5-b3e7-1d02-0000-000000000003 + - name: Grid-Wallet-Signature in: header required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + description: Full API-key stamp built over the prior `payloadToSign` with the current session API keypair. Required on the signed retry; ignored on the initial call. schema: type: string - example: + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in the prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:019542f5-b3e7-1d02-0000-000000000010 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/QuoteRequest' + $ref: '#/components/schemas/AuthSessionRefreshRequest' + examples: + refresh: + summary: Refresh an active session + value: + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '201': - description: Transfer quote created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Quote' - '400': - description: Bad request - Missing or invalid parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Agent policy does not permit this operation - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' - '412': - description: Counterparty doesn't support UMA version - content: - application/json: - schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue + description: New authentication session created successfully. content: application/json: schema: - $ref: '#/components/schemas/Error424' - '500': - description: Internal service error + $ref: '#/components/schemas/AuthSession' + examples: + session: + summary: Refreshed authentication session + value: + id: Session:019542f5-b3e7-1d02-0000-000000000011 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: EMAIL_OTP + encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf + nickname: example@lightspark.com + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:35:00Z' + expiresAt: '2026-04-08T15:50:00Z' + '202': + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the current session API keypair, then echo `requestId` on the signed retry. content: application/json: schema: - $ref: '#/components/schemas/Error500' - /agents/me/quotes/{quoteId}: - parameters: - - name: quoteId - in: path - description: ID of the quote to retrieve - required: true - schema: - type: string - get: - summary: Get agent quote by ID - description: | - Retrieve a quote created by the authenticated agent. Returns 404 if the quote exists but was not created by this agent. - operationId: agentGetQuote - tags: - - Agent Operations - security: - - AgentAuth: [] - responses: - '200': - description: Quote retrieved successfully + $ref: '#/components/schemas/SignedRequestChallenge' + examples: + challenge: + summary: Session refresh challenge + value: + payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' + requestId: Request:019542f5-b3e7-1d02-0000-000000000010 + expiresAt: '2026-04-08T15:35:00Z' + '400': + description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the `BasicAuth` credentials are missing or invalid, when the target session is no longer active and cannot be used for refresh, when the signed retry omits `Grid-Wallet-Signature`, when the provided signature is malformed or does not match the pending refresh challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Quote not found + description: Session not found content: application/json: schema: @@ -6457,75 +6173,100 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/quotes/{quoteId}/execute: - parameters: - - name: quoteId - in: path - required: true - description: The unique identifier of the quote to execute - schema: - type: string - example: Quote:019542f5-b3e7-1d02-0000-000000000001 + /auth/delegated-keys: post: - summary: Execute a quote + summary: Create a delegated signing key description: | - Execute a quote created by the authenticated agent. Requires the EXECUTE_QUOTES permission in the agent's policy. - If the agent's policy requires approval for this amount (based on execution mode or approval thresholds), the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. - Once executed, the quote cannot be cancelled. - operationId: agentExecuteQuote + Delegate Spark token-transaction signing authority for a card funding source backed by an Embedded Wallet internal account to a Grid-custodied P-256 API key. Grid uses the requested card and internal account to identify the wallet funding source, generates the keypair server-side, creates an isolated signer identity holding the public key, then policies granting that identity signing and self-revocation authority. The private key is custodied by Grid and never returned. Both activities must be authorized by the wallet owner, so creation is a three-leg signed-retry flow: + + 1. Call `POST /auth/delegated-keys` with no signature headers. Grid generates the delegated keypair and the response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified credential on the requested Embedded Wallet internal account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The response is a second `202` with a new `payloadToSign`, `requestId`, and `expiresAt`. + + 3. Stamp the new `payloadToSign` with the same session keypair and retry once more with the new `Request-Id`. The signed retry returns `201` with the created `DelegatedKey` in `ACTIVE` status. + + The same request body must be sent on all three legs. A flow abandoned after the second leg leaves the key in `PENDING` status: the signer identity exists but holds no policies, so it cannot sign or revoke itself. Abandoned `PENDING` keys do not block creating another delegated key. After activation, Grid uses the custodied key to authorize signing for the card's Embedded Wallet funding account in place of a session keypair; the platform never handles the key material. + + Each card funding source may have at most one `ACTIVE` delegated key for its Embedded Wallet funding account; revoke the existing active key before creating a new one. A delegated key authorizes raw-payload signing for the wallet and cannot be scoped to amounts or recipients by the public API. Revoke it with `DELETE /auth/delegated-keys/{id}` when no longer needed. + operationId: createDelegatedKey tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: Idempotency-Key + - name: Grid-Wallet-Signature in: header required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified credential on the same internal account. Required on the signed retries; ignored on the initial call. schema: type: string - example: - - name: Grid-Wallet-Signature + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id in: header required: false - description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types. + description: The `requestId` returned in the prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retries; must be paired with `Grid-Wallet-Signature`. schema: type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DelegatedKeyCreateRequest' + examples: + create: + summary: Delegate signing to a Grid-custodied key + value: + cardId: Card:019542f5-b3e7-1d02-0000-000000000010 + internalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + nickname: Card payments key responses: - '200': - description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.' + '201': + description: Delegated key created and policy granted. The key is `ACTIVE` and Grid may use it to stamp card-payment quote executions for this card funding source's Embedded Wallet funding account. content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/DelegatedKey' + examples: + delegatedKey: + summary: Active delegated key + value: + id: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 + cardId: Card:019542f5-b3e7-1d02-0000-000000000010 + fundingSourceId: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + publicKey: 02a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90 + nickname: Card payments key + status: ACTIVE + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:42Z' + '202': + description: Challenge issued for the next leg. Stamp `payloadToSign` and retry the same request with `Grid-Wallet-Signature` and `Request-Id`. + content: + application/json: + schema: + $ref: '#/components/schemas/DelegatedKeySignedRequestChallenge' '400': - description: Bad request - Quote cannot be executed + description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`. + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing on a retry, malformed, or does not match the pending challenge, or when the `Request-Id` does not match an unexpired pending challenge. content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Agent policy does not permit this operation or spending limit exceeded - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Quote not found + description: Card, card funding source, or Embedded Wallet funding account not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: Conflict - Quote already executed, expired, or in invalid state + description: An `ACTIVE` delegated key already exists for this card funding source. Revoke it with `DELETE /auth/delegated-keys/{id}` before creating a new one. content: application/json: schema: @@ -6536,81 +6277,78 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/actions: get: - summary: List agent's own actions - description: | - Retrieve a paginated list of actions submitted by the authenticated agent. Use this to poll for approval decisions after submitting an action that requires approval. - operationId: agentListActions + summary: List delegated signing keys + description: List delegated signing keys for an Embedded Wallet internal account, a card funding source, or both, including `PENDING` keys (user created but policy leg never completed) and `REVOKED` keys. At least one of `accountId` or `fundingSourceId` must be supplied. + operationId: listDelegatedKeys tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: status - in: query - description: Filter by action status - required: false - schema: - $ref: '#/components/schemas/AgentActionStatus' - - name: limit + - name: accountId in: query - description: Maximum number of results to return (default 20, max 100) required: false + description: The id of the internal account whose delegated keys to list. schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor + type: string + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + - name: fundingSourceId in: query - description: Cursor for pagination (returned from previous request) required: false + description: The id of the card funding source whose delegated keys to list. schema: type: string + example: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011 responses: '200': - description: Successful operation + description: Delegated keys matching the supplied filters. Returns an empty `data` array when no matching delegated keys are visible to the caller. content: application/json: schema: - $ref: '#/components/schemas/AgentActionListResponse' - '401': - description: Unauthorized + $ref: '#/components/schemas/DelegatedKeyListResponse' + '400': + description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Error401' + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/actions/{actionId}: - parameters: - - name: actionId - in: path - description: Unique identifier of the agent action - required: true - schema: - type: string + /auth/delegated-keys/{id}: get: - summary: Get an agent action - description: | - Retrieve a specific action submitted by the authenticated agent. Poll this endpoint after submitting an action that requires approval to check whether it has been approved, rejected, or has failed. - operationId: agentGetAction + summary: Get a delegated signing key + description: Retrieve a delegated signing key by its system-generated id. + operationId: getDelegatedKeyById tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the delegated key to retrieve (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys` or `GET /auth/delegated-keys`). + required: true + schema: + type: string + example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/DelegatedKey' '401': description: Unauthorized content: @@ -6618,7 +6356,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Action not found + description: Delegated key not found content: application/json: schema: @@ -6629,69 +6367,42 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/transfer-in: - post: - summary: Create a transfer-in + delete: + summary: Revoke a delegated signing key description: | - Transfer funds from an external account to an internal account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy. - If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. - This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the payment instructions on the internal account to deposit funds. - operationId: agentCreateTransferIn + Revoke an `ACTIVE` delegated signing key. Grid uses the custodied delegated key to authorize deleting its own signer identity. Deleting the identity also removes its API key, after which the delegated key can no longer sign. The response is `204` when revocation completes. + + The underlying signing policies are left in place. Their consensus references the now-deleted signer identity, so they can never authorize anything, and deleting them is unnecessary for correctness or security. + operationId: revokeDelegatedKey tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + - name: id + in: path + description: The id of the delegated key to revoke (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys`). + required: true schema: type: string - example: 550e8400-e29b-41d4-a716-446655440000 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TransferInRequest' - examples: - transferIn: - summary: Transfer from external to internal account - value: - source: - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - amount: 12550 + example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 responses: - '201': - description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. - content: - application/json: - schema: - $ref: '#/components/schemas/AgentAction' + '204': + description: Delegated key revoked. The key can no longer authorize signing. '400': - description: Bad request - Invalid parameters + description: Bad request. Returned when the delegated key has already been revoked or is not `ACTIVE`. content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Agent policy does not permit this operation or spending limit exceeded - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Account not found + description: Delegated key not found content: application/json: schema: @@ -6702,50 +6413,31 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/transfer-out: + /agents: post: - summary: Create a transfer-out + summary: Create an agent description: | - Transfer funds from an internal account to an external account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy. - If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. - operationId: agentCreateTransferOut + Create a new agent with a specified policy. Returns the created agent and a device code that must be redeemed by the agent software to complete installation. + operationId: createAgent tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] - parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. - schema: - type: string - example: 550e8400-e29b-41d4-a716-446655440000 + - BasicAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/TransferOutRequest' - examples: - transferOut: - summary: Transfer from internal to external account - value: - source: - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - destination: - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: 12550 + $ref: '#/components/schemas/AgentCreateRequest' responses: '201': - description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. + description: Agent created successfully content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/AgentCreateResponse' '400': - description: Bad request - Invalid parameters + description: Bad request content: application/json: schema: @@ -6756,47 +6448,67 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Agent policy does not permit this operation or spending limit exceeded - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' - '404': - description: Account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/internal-accounts: get: - summary: List agent's internal accounts - description: | - Retrieve the internal accounts belonging to the customer this agent operates on behalf of. Use this to discover available source accounts for transfers and quotes, and to verify which accounts are accessible under the agent's `accountRestrictions` policy. - operationId: agentListInternalAccounts + summary: List agents + description: Retrieve a paginated list of agents for the authenticated platform. + operationId: listAgents tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: currency + - name: customerId in: query - description: Filter by currency code + description: Filter by customer ID required: false schema: type: string - - name: type + - name: isPaused 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 paused status required: false schema: - $ref: '#/components/schemas/InternalAccountType' + type: boolean + - name: isConnected + in: query + description: Filter by connection status (whether the device code has been redeemed) + required: false + schema: + type: boolean + - name: createdAfter + in: query + description: Filter agents created after this timestamp (inclusive) + required: false + schema: + type: string + format: date-time + - name: createdBefore + in: query + description: Filter agents created before this timestamp (inclusive) + required: false + schema: + type: string + format: date-time + - name: updatedAfter + in: query + description: Filter agents updated after this timestamp (inclusive) + required: false + schema: + type: string + format: date-time + - name: updatedBefore + in: query + description: Filter agents updated before this timestamp (inclusive) + required: false + schema: + type: string + format: date-time - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -6818,7 +6530,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/InternalAccountListResponse' + $ref: '#/components/schemas/AgentListResponse' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -6831,23 +6549,43 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/external-accounts: + /agents/approvals: get: - summary: List agent external accounts + summary: List agent transaction approval requests description: | - Retrieve a paginated list of external accounts belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. - operationId: agentListExternalAccounts + Retrieve a paginated list of agent actions that require platform approval. Filter by `agentId` or `customerId` to scope results to a specific agent or customer. Approve or reject individual actions via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`. + operationId: listAgentApprovals tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: currency + - name: agentId in: query - description: Filter by currency code + description: Filter by agent ID + required: false + schema: + type: string + - name: customerId + in: query + description: Filter by customer ID + required: false + schema: + type: string + - name: startDate + in: query + description: Filter by start date (inclusive) in ISO 8601 format + required: false + schema: + type: string + format: date-time + - name: endDate + in: query + description: Filter by end date (inclusive) in ISO 8601 format required: false schema: type: string + format: date-time - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -6863,13 +6601,23 @@ paths: required: false schema: type: string - responses: - '200': - description: Successful operation + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc + responses: + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' + $ref: '#/components/schemas/AgentActionListResponse' '400': description: Bad request - Invalid parameters content: @@ -6888,105 +6636,58 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - post: - summary: Add an external account + /agents/me: + get: + summary: Get current agent description: | - Register a new external bank account or wallet for the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. The `customerId` field is optional and will be inferred from the agent's associated customer if omitted. - operationId: agentCreateExternalAccount + Retrieve the authenticated agent's own profile, policy, and current usage. This endpoint is called by the agent software itself using its own credentials (obtained via device code redemption) rather than platform credentials. + operationId: getAgentMe tags: - Agent Operations security: - AgentAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ExternalAccountCreateRequest' - examples: - usBankAccount: - summary: Create external US bank account - value: - currency: USD - accountInfo: - accountType: USD_ACCOUNT - accountNumber: '12345678901' - routingNumber: '123456789' - bankAccountType: CHECKING - bankName: Chase Bank - beneficiary: - beneficiaryType: INDIVIDUAL - fullName: John Doe - birthDate: '1990-01-15' - nationality: US - address: - line1: 123 Main Street - city: San Francisco - state: CA - postalCode: '94105' - country: US - sparkWallet: - summary: Create external Spark wallet - value: - currency: BTC - accountInfo: - accountType: SPARK_WALLET - address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu responses: - '201': - description: External account created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ExternalAccount' - '400': - description: Bad request + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/Agent' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - External account already exists - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/external-accounts/{externalAccountId}: + /agents/{agentId}: parameters: - - name: externalAccountId + - name: agentId in: path - description: System-generated unique external account identifier + description: System-generated unique agent identifier required: true schema: type: string get: - summary: Get agent external account by ID - description: | - Retrieve an external account belonging to the authenticated agent's customer. Returns 404 if the account exists but belongs to a different customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. - operationId: agentGetExternalAccount + summary: Get agent by ID + description: Retrieve an agent by its system-generated ID. + operationId: getAgentById tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] + - BasicAuth: [] responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/Agent' '401': description: Unauthorized content: @@ -6994,7 +6695,52 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Agent not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + patch: + summary: Update agent + description: Update an agent's name or paused state. + operationId: updateAgent + tags: + - Agent Management + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AgentUpdateRequest' + responses: + '200': + description: Agent updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Agent' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Agent not found content: application/json: schema: @@ -7006,17 +6752,16 @@ paths: schema: $ref: '#/components/schemas/Error500' delete: - summary: Delete agent external account - description: | - Delete an external account belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. - operationId: agentDeleteExternalAccount + summary: Delete agent + description: Permanently delete an agent. Connected agent software will lose access immediately. + operationId: deleteAgent tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] + - BasicAuth: [] responses: '204': - description: External account deleted successfully + description: Agent deleted successfully '401': description: Unauthorized content: @@ -7024,7 +6769,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Agent not found content: application/json: schema: @@ -7035,18 +6780,21 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /cards: - post: - summary: Issue a card + /agents/{agentId}/policy: + parameters: + - name: agentId + in: path + description: System-generated unique agent identifier + required: true + schema: + type: string + patch: + summary: Update agent policy description: | - Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`. - - If any funding source is an Embedded Wallet internal account, the cardholder must authorize Grid to sign Spark token transactions for that card funding source by completing the delegated-key creation flow with `POST /auth/delegated-keys`. Until an active delegated key exists for that funding source, Authorization Decisioning cannot use it to fund card transactions. - - New cards start in `state: "PROCESSING"` while the card issuer provisions the card. The `card.state_change` webhook fires on each state transition, including the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails). - operationId: createCard + Partially update an agent's policy. Only provided fields will be updated; omitted fields retain their current values. Policy changes take effect immediately. + operationId: updateAgentPolicy tags: - - Cards + - Agent Management security: - BasicAuth: [] requestBody: @@ -7054,25 +6802,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CardCreateRequest' - examples: - virtualCard: - summary: Issue a virtual card with one funding source - value: - cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCardId: card-emp-aary-001 - form: VIRTUAL - fundingSources: - - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + $ref: '#/components/schemas/AgentPolicyUpdateRequest' responses: - '201': - description: Card created successfully. Newly-created cards start in `PROCESSING` while the issuer provisions them. Cards funded by an Embedded Wallet internal account also require an active delegated key for that funding source before Authorization Decisioning can use it. + '200': + description: Agent policy updated successfully content: application/json: schema: - $ref: '#/components/schemas/Card' + $ref: '#/components/schemas/Agent' '400': - description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters. + description: Bad request content: application/json: schema: @@ -7083,86 +6822,44 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error + '404': + description: Agent not found content: application/json: schema: - $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Card issuance is not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error501' - get: - summary: List cards + $ref: '#/components/schemas/Error500' + /agents/{agentId}/device-codes: + parameters: + - name: agentId + in: path + description: System-generated unique agent identifier + required: true + schema: + type: string + post: + summary: Regenerate a device code description: | - Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller. - operationId: listCards + Generate a new device code for an existing agent. Use this when the original device code has expired before being redeemed, or when the agent software needs to be reinstalled. Any previously issued unredeemed device codes for this agent are invalidated. + operationId: regenerateAgentDeviceCode tags: - - Cards + - Agent Management security: - BasicAuth: [] - parameters: - - name: cardholderId - in: query - description: Filter by cardholder (customer) id. - required: false - schema: - type: string - - name: accountId - in: query - description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id. - required: false - schema: - type: string - - name: platformCardId - in: query - description: Filter by platform-specific card identifier. - required: false - schema: - type: string - - name: state - in: query - description: Filter by card state. - required: false - schema: - $ref: '#/components/schemas/CardState' - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string - - name: sortOrder - in: query - description: Order to sort results in - required: false - schema: - type: string - enum: - - asc - - desc - default: desc responses: - '200': - description: Successful operation + '201': + description: New device code generated successfully content: application/json: schema: - $ref: '#/components/schemas/CardListResponse' + $ref: '#/components/schemas/AgentDeviceCode' '400': - description: Bad request - Invalid parameters + description: Bad request content: application/json: schema: @@ -7173,41 +6870,62 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error + '404': + description: Agent not found content: application/json: schema: - $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + $ref: '#/components/schemas/Error404' + '409': + description: Conflict - Agent already has an active connection and cannot regenerate a device code content: application/json: schema: - $ref: '#/components/schemas/Error501' - /cards/{id}: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /agents/{agentId}/actions/{actionId}/approve: parameters: - - name: id + - name: agentId in: path - description: System-generated unique card identifier + description: System-generated unique agent identifier required: true schema: type: string - get: - summary: Get a card - description: Retrieve a card by its system-generated id. To display the card's full PAN, CVV, and expiry to the cardholder, request a reveal with `POST /cards/{id}/reveal` — the card resource itself never carries the reveal URL. - operationId: getCardById + - name: actionId + in: path + description: Unique identifier of the agent action to approve + required: true + schema: + type: string + post: + summary: Approve an agent action + description: | + Approve a pending agent action, allowing Grid to proceed with execution. The action must have status `PENDING_APPROVAL`. Once approved, Grid executes the underlying operation (quote execution or transfer) and the action transitions to `APPROVED`. + For `EXECUTE_QUOTE` actions, note that the underlying quote may have expired between submission and approval — in that case the action will transition to `FAILED` instead. + This endpoint is called by the platform's backend using platform credentials, not by the agent itself. + operationId: approveAgentAction tags: - - Cards + - Agent Management security: - BasicAuth: [] responses: '200': - description: Successful operation + description: Action approved successfully. Returns the updated AgentAction. content: application/json: schema: - $ref: '#/components/schemas/Card' + $ref: '#/components/schemas/AgentAction' + '400': + description: Bad request - Action cannot be approved + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -7215,128 +6933,80 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Card not found + description: Agent or action not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '500': - description: Internal service error + '409': + description: Conflict - Action is not pending approval or has already been processed content: application/json: schema: - $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error501' - patch: - summary: Update a card + $ref: '#/components/schemas/Error500' + /agents/{agentId}/actions/{actionId}/reject: + parameters: + - name: agentId + in: path + description: System-generated unique agent identifier + required: true + schema: + type: string + - name: actionId + in: path + description: Unique identifier of the agent action to reject + required: true + schema: + type: string + post: + summary: Reject an agent action description: | - Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied. - - - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`. - - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`. - - Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`): - - 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - - 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`. - - Effects: - - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed. - - `state: ACTIVE`: normal authorization behavior resumes. - - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys. - - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding. - - The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated. - operationId: updateCardById + Reject a pending agent action, preventing execution. The action must have status `PENDING_APPROVAL`. Once rejected, the action transitions to `REJECTED` and the underlying operation is not executed. + This endpoint is called by the platform's backend using platform credentials, not by the agent itself. + operationId: rejectAgentAction tags: - - Cards + - Agent Management security: - BasicAuth: [] - parameters: - - name: Grid-Wallet-Signature - in: header - required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/CardUpdateRequest' - examples: - freeze: - summary: Freeze an active card - value: - state: FROZEN - unfreeze: - summary: Unfreeze a frozen card - value: - state: ACTIVE - updateFundingSources: - summary: Replace the card's bound funding sources - value: - fundingSources: - - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - - InternalAccount:019542f5-b3e7-1d02-0000-000000000003 - freezeAndUpdateSources: - summary: Freeze the card and replace its funding sources in one call - value: - state: FROZEN - fundingSources: - - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - close: - summary: Permanently close the card - value: - state: CLOSED + $ref: '#/components/schemas/AgentActionRejectRequest' responses: '200': - description: Signed retry accepted. Returns the updated card. - content: - application/json: - schema: - $ref: '#/components/schemas/Card' - '202': - description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry. + description: Action rejected successfully. Returns the updated AgentAction. content: application/json: schema: - $ref: '#/components/schemas/SignedRequestChallenge' + $ref: '#/components/schemas/AgentAction' '400': - description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters. + description: Bad request - Action cannot be rejected content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Card not found + description: Agent or action not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.' + description: Conflict - Action is not pending approval or has already been processed content: application/json: schema: @@ -7347,54 +7017,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. - content: - application/json: - schema: - $ref: '#/components/schemas/Error501' - /cards/{id}/reveal: + /agents/device-codes/{code}/status: parameters: - - name: id + - name: code in: path - description: System-generated unique card identifier + description: The device code to check required: true schema: type: string - post: - summary: Reveal card details - description: |- - Mint a signed, short-lived URL for the card processor's iframe that displays the card's full PAN, CVV, and expiry to the cardholder. This is the only way to obtain a reveal URL — the `Card` resource never carries one. - - Request the reveal right before rendering the iframe and render the returned `panEmbedUrl` immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log the URL — it is a bearer secret for the full card details. The card data renders inside the processor's iframe and never crosses Grid's or your servers. - - Every reveal is audit-logged with the requesting actor. - operationId: revealCard + get: + summary: Get device code status + description: | + Check whether a device code has been redeemed. Use this to poll for agent installation completion after creating an agent. + operationId: getAgentDeviceCodeStatus tags: - - Cards + - Agent Management security: - BasicAuth: [] responses: '200': - description: Reveal URL minted. + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/CardRevealResponse' + $ref: '#/components/schemas/AgentDeviceCodeStatusResponse' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden. The session has no attributable actor to audit the reveal against (for example, an impersonated dashboard session). - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Card not found + description: Device code not found content: application/json: schema: @@ -7405,90 +7059,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. - content: - application/json: - schema: - $ref: '#/components/schemas/Error501' - /sandbox/cards/{id}/simulate/authorization: - post: - summary: Simulate a card authorization - description: | - Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip. - - The decisioning outcome is controlled by the last three characters of `merchant.descriptor`: - - | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved | - - Production returns `404` on this path. - operationId: sandboxSimulateCardAuthorization - tags: - - Sandbox - security: - - BasicAuth: [] - parameters: - - name: id - in: path - required: true - description: The id of the card to simulate an authorization against. - schema: - type: string - example: Card:019542f5-b3e7-1d02-0000-000000000010 - requestBody: + /agents/device-codes/{code}/redeem: + parameters: + - name: code + in: path + description: The device code to redeem required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SandboxCardAuthorizationRequest' - examples: - coffeeAuth: - summary: Approved $12.50 auth at a coffee shop - value: - amount: 1250 - currency: - code: USD - merchant: - descriptor: BLUE BOTTLE COFFEE SF - mcc: '5814' - country: US - declinedInsufficientFunds: - summary: Declined — insufficient funds (descriptor suffix `002`) - value: - amount: 50000 - currency: - code: USD - merchant: - descriptor: AMAZON RETAIL US-002 - mcc: '5942' - country: US + schema: + type: string + post: + summary: Redeem device code + description: | + Redeem a device code to obtain agent credentials. This endpoint is called by the agent software during installation. On success, returns a Bearer access token that the agent uses for all subsequent API calls. The token is returned only once and must be stored securely. + This endpoint does not require platform authentication — the device code itself serves as proof of authorization. + operationId: redeemAgentDeviceCode + tags: + - Agent Management + security: [] responses: '200': - description: Simulated authorization processed. Returns the resulting card transaction. + description: Device code redeemed successfully content: application/json: schema: - $ref: '#/components/schemas/CardTransaction' + $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse' '400': - description: Bad request - Invalid parameters + description: Bad request (e.g., code already redeemed or expired) content: application/json: schema: $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Card not found (also returned in production for this path) + description: Device code not found content: application/json: schema: @@ -7499,54 +7101,99 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/cards/{id}/simulate/clearing: - post: - summary: Simulate a card clearing + /agents/me/transactions: + get: + summary: List agent transactions description: | - Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment. - - - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth). - - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted. - - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases). - - Production returns `404` on this path. - operationId: sandboxSimulateCardClearing + Retrieve a paginated list of transactions for the authenticated agent's customer. Results are automatically scoped to the agent's associated customer — no customer filter is needed or accepted. + operationId: agentListTransactions tags: - - Sandbox + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] parameters: - - name: id - in: path - required: true - description: The id of the card the clearing applies to. + - name: accountIdentifier + in: query + description: Filter by account identifier (matches either sender or receiver) + required: false schema: type: string - example: Card:019542f5-b3e7-1d02-0000-000000000010 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SandboxCardClearingRequest' - examples: - tipOnTopClearing: - summary: Clearing larger than auth — exercises post-hoc pull - value: - cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 - amount: 1500 - authorizationExpiry: - summary: Clearing of 0 — exercises authorization expiry - value: - cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 - amount: 0 + - name: senderAccountIdentifier + in: query + description: Filter by sender account identifier + required: false + schema: + type: string + - name: receiverAccountIdentifier + in: query + description: Filter by receiver account identifier + required: false + schema: + type: string + - name: status + in: query + description: Filter by transaction status + required: false + schema: + $ref: '#/components/schemas/TransactionStatus' + - name: type + in: query + description: Filter by transaction type + required: false + schema: + $ref: '#/components/schemas/TransactionType' + - name: reference + in: query + description: Filter by reference + required: false + schema: + type: string + - name: startDate + in: query + description: Filter by start date (inclusive) in ISO 8601 format + required: false + schema: + type: string + format: date-time + - name: endDate + in: query + description: Filter by end date (inclusive) in ISO 8601 format + required: false + schema: + type: string + format: date-time + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc responses: '200': - description: Simulated clearing processed. Returns the updated card transaction. + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/CardTransaction' + $ref: '#/components/schemas/TransactionListResponse' '400': description: Bad request - Invalid parameters content: @@ -7559,14 +7206,44 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error403' + $ref: '#/components/schemas/Error500' + /agents/me/transactions/{transactionId}: + parameters: + - name: transactionId + in: path + description: Unique identifier of the transaction + required: true + schema: + type: string + get: + summary: Get agent transaction by ID + description: | + Retrieve a specific transaction belonging to the authenticated agent's customer. Returns 404 if the transaction exists but belongs to a different customer. + operationId: agentGetTransaction + tags: + - Agent Operations + security: + - AgentAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionOneOf' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' '404': - description: Card or card transaction not found + description: Transaction not found content: application/json: schema: @@ -7577,47 +7254,41 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/cards/{id}/simulate/return: + /agents/me/quotes: post: - summary: Simulate a card return + summary: Create a transfer quote description: | - Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund). - - Production returns `404` on this path. - operationId: sandboxSimulateCardReturn + Generate a quote for a cross-currency transfer on behalf of the authenticated agent's customer. Accounts referenced in the request must belong to the agent's customer. Requires the CREATE_QUOTES permission in the agent's policy. + If the agent's defaultExecutionMode is APPROVAL_REQUIRED, or the quote amount exceeds the agent's approvalThresholds, the resulting transaction will require explicit approval before funds move. + operationId: agentCreateQuote tags: - - Sandbox + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] parameters: - - name: id - in: path - required: true - description: The id of the card the return applies to. - schema: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: type: string - example: Card:019542f5-b3e7-1d02-0000-000000000010 + example: requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/SandboxCardReturnRequest' - examples: - fullRefund: - summary: Full refund of a $15.00 settled transaction - value: - cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 - amount: 1500 + $ref: '#/components/schemas/QuoteRequest' responses: - '200': - description: Simulated return processed. Returns the updated card transaction. + '201': + description: Transfer quote created successfully content: application/json: schema: - $ref: '#/components/schemas/CardTransaction' + $ref: '#/components/schemas/Quote' '400': - description: Bad request - Invalid parameters + description: Bad request - Missing or invalid parameters content: application/json: schema: @@ -7629,13 +7300,61 @@ paths: schema: $ref: '#/components/schemas/Error401' '403': - description: Forbidden - request was made with a production platform token + description: Forbidden - Agent policy does not permit this operation content: application/json: schema: $ref: '#/components/schemas/Error403' + '412': + description: Counterparty doesn't support UMA version + content: + application/json: + schema: + $ref: '#/components/schemas/Error412' + '424': + description: Counterparty issue + content: + application/json: + schema: + $ref: '#/components/schemas/Error424' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /agents/me/quotes/{quoteId}: + parameters: + - name: quoteId + in: path + description: ID of the quote to retrieve + required: true + schema: + type: string + get: + summary: Get agent quote by ID + description: | + Retrieve a quote created by the authenticated agent. Returns 404 if the quote exists but was not created by this agent. + operationId: agentGetQuote + tags: + - Agent Operations + security: + - AgentAuth: [] + responses: + '200': + description: Quote retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Quote' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' '404': - description: Card or card transaction not found + description: Quote not found content: application/json: schema: @@ -7646,51 +7365,75 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /stablecoin-provider-accounts: + /agents/me/quotes/{quoteId}/execute: + parameters: + - name: quoteId + in: path + required: true + description: The unique identifier of the quote to execute + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Link a stablecoin provider account + summary: Execute a quote description: | - Link provider API credentials for the authenticated platform. Provider credentials are account-scoped and can be reused for multiple stablecoins. Currently, the only supported provider is `BRALE`; the provider environment is derived from the authenticated Grid platform mode. - operationId: linkStablecoinProviderAccount + Execute a quote created by the authenticated agent. Requires the EXECUTE_QUOTES permission in the agent's policy. + If the agent's policy requires approval for this amount (based on execution mode or approval thresholds), the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. + Once executed, the quote cannot be cancelled. + operationId: agentExecuteQuote tags: - - Stablecoins + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] parameters: - name: Idempotency-Key in: header - description: Idempotency key for retrying this request safely. Replays return the prior result; conflicting payloads are rejected. - required: true + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. schema: type: string - maxLength: 255 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/StablecoinProviderAccountLinkRequest' + example: + - name: Grid-Wallet-Signature + in: header + required: false + description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 responses: - '201': - description: Stablecoin provider account linked + '200': + description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.' content: application/json: schema: - $ref: '#/components/schemas/StablecoinProviderAccount' + $ref: '#/components/schemas/AgentAction' '400': - description: Bad request + description: Bad request - Quote cannot be executed content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`. content: application/json: schema: $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - Agent policy does not permit this operation or spending limit exceeded + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Quote not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '409': - description: Conflict + description: Conflict - Quote already executed, expired, or in invalid state content: application/json: schema: @@ -7701,25 +7444,23 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /agents/me/actions: get: - summary: List stablecoin provider account links - description: Retrieve stablecoin provider account links for the authenticated platform. - operationId: listStablecoinProviderAccounts + summary: List agent's own actions + description: | + Retrieve a paginated list of actions submitted by the authenticated agent. Use this to poll for approval decisions after submitting an action that requires approval. + operationId: agentListActions tags: - - Stablecoins + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] parameters: - - name: provider - in: query - required: false - schema: - $ref: '#/components/schemas/StablecoinProvider' - name: status in: query + description: Filter by action status required: false schema: - $ref: '#/components/schemas/StablecoinProviderAccountStatus' + $ref: '#/components/schemas/AgentActionStatus' - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -7741,13 +7482,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/StablecoinProviderAccountListResponse' - '400': - description: Bad request - Invalid parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/AgentActionListResponse' '401': description: Unauthorized content: @@ -7760,29 +7495,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /stablecoin-provider-accounts/{stablecoinProviderAccountId}: + /agents/me/actions/{actionId}: parameters: - - name: stablecoinProviderAccountId + - name: actionId in: path - description: System-generated stablecoin provider account link identifier + description: Unique identifier of the agent action required: true schema: type: string get: - summary: Get a stablecoin provider account link - description: Retrieve a stablecoin provider account link by id for the authenticated platform. - operationId: getStablecoinProviderAccount + summary: Get an agent action + description: | + Retrieve a specific action submitted by the authenticated agent. Poll this endpoint after submitting an action that requires approval to check whether it has been approved, rejected, or has failed. + operationId: agentGetAction tags: - - Stablecoins + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/StablecoinProviderAccount' + $ref: '#/components/schemas/AgentAction' '401': description: Unauthorized content: @@ -7790,7 +7526,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Stablecoin provider account link not found + description: Action not found content: application/json: schema: @@ -7801,580 +7537,316 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' -webhooks: - agent-action: + /agents/me/transfer-in: post: - summary: Agent action pending approval webhook + summary: Create a transfer-in description: | - Fired when an agent submits an action that requires platform approval before Grid will execute it. Use this to send a push notification to the customer so they can review and approve or reject the action in your app. - 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. - - The payload contains the full `AgentAction` — including the embedded quote or transfer details — so you can render the approval UI without a second API call. Approve or reject via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`. - operationId: agentActionWebhook + Transfer funds from an external account to an internal account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy. + If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. + This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the payment instructions on the internal account to deposit funds. + operationId: agentCreateTransferIn tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] + - AgentAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/AgentActionWebhook' + $ref: '#/components/schemas/TransferInRequest' examples: - pendingApproval: - summary: Agent action pending approval + transferIn: + summary: Transfer from external to internal account value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000020 - type: AGENT_ACTION.PENDING_APPROVAL - timestamp: '2025-10-03T15:00:00Z' - data: - id: AgentAction:019542f5-b3e7-1d02-0000-000000000099 - agentId: Agent:019542f5-b3e7-1d02-0000-000000000042 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000010 - platformCustomerId: user-a1b2c3 - status: PENDING_APPROVAL - type: EXECUTE_QUOTE - quote: - id: Quote:019542f5-b3e7-1d02-0000-000000000006 - status: PENDING - expiresAt: '2025-10-03T15:00:30Z' - createdAt: '2025-10-03T15:00:00Z' - source: - sourceType: ACCOUNT - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - sendingCurrency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - receivingCurrency: - code: INR - name: Indian Rupee - symbol: ₹ - decimals: 2 - totalSendingAmount: 50000 - totalReceivingAmount: 4625000 - exchangeRate: 92.5 - feesIncluded: 250 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000099 - createdAt: '2025-10-03T15:00:00Z' - updatedAt: '2025-10-03T15:00:00Z' + source: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + amount: 12550 responses: - '200': - description: Webhook received and acknowledged. + '201': + description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. + content: + application/json: + schema: + $ref: '#/components/schemas/AgentAction' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + '403': + description: Forbidden - Agent policy does not permit this operation or spending limit exceeded content: application/json: schema: - $ref: '#/components/schemas/Error409' - incoming-payment: + $ref: '#/components/schemas/Error403' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /agents/me/transfer-out: post: - summary: Incoming payment webhook and approval mechanism + summary: Create a transfer-out description: | - Webhook that is called when an incoming payment is received by a customer's UMA address. - 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. - - ### Payment Approval Flow - When a transaction has `status: "PENDING"`, this webhook serves as an approval mechanism: - - 1. The client should check the `counterpartyInformation` against their requirements - 2. To APPROVE the payment synchronously, return a 200 OK response - 3. To REJECT the payment, return a 403 Forbidden response with an Error object - 4. To request more information, return a 422 Unprocessable Entity with specific missing fields - 5. To process the payment asynchronously, return a 202 Accepted response and then call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint within 5 seconds. Note that synchronous approval/rejection is preferred where possible. - - The Grid system will proceed or cancel the payment based on your response. - - For transactions with other statuses (COMPLETED, FAILED, REFUNDED), this webhook is purely informational. - operationId: incomingPaymentWebhook + Transfer funds from an internal account to an external account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy. + If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. + operationId: agentCreateTransferOut tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] + - AgentAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/IncomingPaymentWebhook' + $ref: '#/components/schemas/TransferOutRequest' examples: - pendingPayment: - summary: Pending payment example requiring approval - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT.PENDING - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000005 - status: PENDING - type: INCOMING - direction: CREDIT - destination: - destinationType: UMA_ADDRESS - umaAddress: $recipient@uma.domain - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - senderUmaAddress: $sender@external.domain - receiverUmaAddress: $recipient@uma.domain - receivedAmount: - amount: 50000 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - counterpartyInformation: - FULL_NAME: John Sender - BIRTH_DATE: '1985-06-15' - NATIONALITY: US - reconciliationInstructions: - reference: REF-123456789 - requestedReceiverCustomerInfoFields: - - name: NATIONALITY - mandatory: true - - name: POSTAL_ADDRESS - mandatory: false - incomingCompletedPayment: - summary: Completed payment notification - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000005 - status: COMPLETED - type: INCOMING - direction: CREDIT - destination: - destinationType: UMA_ADDRESS - umaAddress: $recipient@uma.domain - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - senderUmaAddress: $sender@external.domain - receiverUmaAddress: $recipient@uma.domain - receivedAmount: - amount: 50000 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - settledAt: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: Payment for services - reconciliationInstructions: - reference: REF-123456789 - incomingCompletedCryptoPayment: - summary: Completed payment funded from an external crypto wallet + transferOut: + summary: Transfer from internal to external account value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000009 - type: INCOMING_PAYMENT.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000006 - status: COMPLETED - type: INCOMING - direction: CREDIT - source: - sourceType: REALTIME_FUNDING - currency: USDC - onChainTransaction: - transactionHash: 7RJWhvQBQPEjJmki5fhBboGBWRJhmcFkMvrr4Fu3tMSJ5EdynMEiYSyiWAH9GpcbHpeUzeSQF9ZY6q4x8AhBskUf - network: SOLANA - destination: - destinationType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - receivedAmount: - amount: 100000 - currency: - code: USDC - name: USD Coin - symbol: '' - decimals: 6 - settledAt: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: USDC deposit from self-custody wallet + source: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + destination: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: 12550 responses: - '200': - description: | - Webhook received successfully. - For PENDING transactions, this indicates approval to proceed with the payment. - If `requestedReceiverCustomerInfoFields` were present in the webhook request, the corresponding fields for the recipient must be included in this response in the `receiverCustomerInfo` object. + '201': + description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. content: application/json: schema: - $ref: '#/components/schemas/IncomingPaymentWebhookResponse' - '202': - description: | - Webhook received and will be processed asynchronously. The synchronous 200 response should be preferred where possible. This asycnhronous path should only be used in - cases where the platform's architecture requires async (but still very quick) processing before approving or rejecting the payment. - The platform must call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint to approve or reject the payment within 5 seconds or the payment will be automatically rejected. + $ref: '#/components/schemas/AgentAction' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '403': - description: | - Forbidden - Payment rejected by the client. - Only applicable for PENDING transactions. + description: Forbidden - Agent policy does not permit this operation or spending limit exceeded content: application/json: schema: - $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + $ref: '#/components/schemas/Error403' + '404': + description: Account not found content: application/json: schema: - $ref: '#/components/schemas/Error409' - '422': - description: | - Unprocessable Entity - Additional counterparty information required. - Only applicable for PENDING transactions. + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/IncomingPaymentWebhookUnprocessableResponse' - outgoing-payment: - post: - summary: Outgoing payment status webhook + $ref: '#/components/schemas/Error500' + /agents/me/internal-accounts: + get: + summary: List agent's internal accounts description: | - Webhook that is called when an outgoing payment's status changes. - 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. - - This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded. - operationId: outgoingPaymentWebhook + Retrieve the internal accounts belonging to the customer this agent operates on behalf of. Use this to discover available source accounts for transfers and quotes, and to verify which accounts are accessible under the agent's `accountRestrictions` policy. + operationId: agentListInternalAccounts tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OutgoingPaymentWebhook' - examples: - outgoingCompletedPayment: - summary: Completed outgoing payment - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000005 - status: COMPLETED - type: OUTGOING - direction: DEBIT - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - senderUmaAddress: $sender@uma.domain - receiverUmaAddress: $recipient@external.domain - sentAmount: - amount: 10550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - receivedAmount: - amount: 9706 - currency: - code: EUR - name: Euro - symbol: € - decimals: 2 - exchangeRate: 0.92 - quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - settledAt: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: 'Payment for invoice #1234' - paymentInstructions: [] - rateDetails: - counterpartyMultiplier: 1.08 - counterpartyFixedFee: 10 - gridApiMultiplier: 0.925 - gridApiFixedFee: 10 - gridApiVariableFeeRate: 0.003 - gridApiVariableFeeAmount: 30 - outgoingCompletedCryptoPayment: - summary: Completed crypto payout to an external wallet - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: OUTGOING_PAYMENT.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000009 - status: COMPLETED - type: OUTGOING - direction: DEBIT - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:c34dcbd6-dced-4ec4-b756-3c3a9ea3d789 - onChainTransaction: - transactionHash: h82pJGF9p7kpzb6eU326EFZf2cDnimbTFVeJtx1qtBmUNJAEqN76R7PwPfHt3oWb8R6cKvhgyxQdDn53jFrK6wFx - network: SOLANA - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - sentAmount: - amount: 100000 - currency: - code: USDC - name: USD Coin - symbol: '' - decimals: 6 - receivedAmount: - amount: 100000 - currency: - code: USDC - name: USD Coin - symbol: '' - decimals: 6 - quoteId: Quote:019542f5-b3e7-1d02-0000-000000000010 - settledAt: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: USDC withdrawal to self-custody wallet - paymentInstructions: [] - failedPayment: - summary: Failed outgoing payment - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT.FAILED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000005 - status: FAILED - type: OUTGOING - direction: DEBIT - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - senderUmaAddress: $sender@uma.domain - receiverUmaAddress: $recipient@external.domain - sentAmount: - amount: 10550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - createdAt: '2025-08-15T14:25:18Z' - quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - failureReason: QUOTE_EXECUTION_FAILED + - AgentAuth: [] + parameters: + - name: currency + in: query + description: Filter by currency code + required: false + schema: + 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. + required: false + schema: + $ref: '#/components/schemas/InternalAccountType' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string responses: '200': - description: Webhook received successfully - '400': - description: Bad request + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/InternalAccountListResponse' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error409' - test-webhook: - post: - summary: Test webhook for integration verification + $ref: '#/components/schemas/Error500' + /agents/me/external-accounts: + get: + summary: List agent external accounts description: | - Webhook that is sent once to verify your webhook endpoint is correctly set up. - This is sent when you configure or update your platform settings with a webhook URL. - - ### Authentication - The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by the Grid API. - 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. - - This webhook is purely for testing your endpoint integration and signature verification. - operationId: testWebhook + Retrieve a paginated list of external accounts belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. + operationId: agentListExternalAccounts tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TestWebhookRequest' - examples: - testWebhook: - summary: Test webhook example - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000001 - type: TEST - timestamp: '2025-08-15T14:32:00Z' - data: {} + - AgentAuth: [] + parameters: + - name: currency + in: query + description: Filter by currency code + required: false + schema: + type: string + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string responses: '200': - description: Webhook received successfully. This confirms your webhook endpoint is properly configured. + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccountListResponse' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error409' - bulk-upload: + $ref: '#/components/schemas/Error500' post: - summary: Bulk upload status webhook + summary: Add an external account description: | - Webhook that is called when a bulk customer upload job completes or fails. - 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. - - This webhook is sent when a bulk upload job completes or fails, providing detailed information about the results. - operationId: bulkUploadWebhook + Register a new external bank account or wallet for the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. The `customerId` field is optional and will be inferred from the agent's associated customer if omitted. + operationId: agentCreateExternalAccount tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] + - AgentAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/BulkUploadWebhook' + $ref: '#/components/schemas/ExternalAccountCreateRequest' examples: - completedUpload: - summary: Successful bulk upload completion + usBankAccount: + summary: Create external US bank account value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Job:019542f5-b3e7-1d02-0000-000000000006 - status: COMPLETED - progress: - total: 5000 - processed: 5000 - successful: 5000 - failed: 0 - errors: [] - failedUpload: - summary: Failed bulk upload + currency: USD + accountInfo: + accountType: USD_ACCOUNT + accountNumber: '12345678901' + routingNumber: '123456789' + bankAccountType: CHECKING + bankName: Chase Bank + beneficiary: + beneficiaryType: INDIVIDUAL + fullName: John Doe + birthDate: '1990-01-15' + nationality: US + address: + line1: 123 Main Street + city: San Francisco + state: CA + postalCode: '94105' + country: US + sparkWallet: + summary: Create external Spark wallet value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD.FAILED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Job:019542f5-b3e7-1d02-0000-000000000006 - status: FAILED - progress: - total: 5000 - processed: 5000 - successful: 0 - failed: 5000 - errors: - - correlationId: row_1 - error: - code: invalid_csv_format - message: Invalid CSV format - details: - reason: missing_required_column - column: umaAddress + currency: BTC + accountInfo: + accountType: SPARK_WALLET + address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu responses: - '200': - description: Webhook received successfully + '201': + description: External account created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' '400': description: Bad request content: @@ -8382,121 +7854,1557 @@ webhooks: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate id) + description: Conflict - External account already exists content: application/json: schema: $ref: '#/components/schemas/Error409' - invitation-claimed: - post: - summary: Invitation claimed webhook + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /agents/me/external-accounts/{externalAccountId}: + parameters: + - name: externalAccountId + in: path + description: System-generated unique external account identifier + required: true + schema: + type: string + get: + summary: Get agent external account by ID description: | - Webhook that is called when an invitation is claimed by a customer. - This endpoint should be implemented by platform clients of the Grid API. - - When a customer claims an invitation, this webhook is triggered to notify the platform that: - 1. The invitation has been successfully claimed - 2. The invitee UMA address is now associated with the invitation - 3. The invitation status has changed from PENDING to CLAIMED - - This allows platforms to: - - Track invitation usage and conversion rates - - Trigger onboarding flows for new customers who joined via invitation - - Apply referral bonuses or rewards to the inviter - - Update their UI to reflect the claimed status - - ### 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. - operationId: invitationClaimedWebhook + Retrieve an external account belonging to the authenticated agent's customer. Returns 404 if the account exists but belongs to a different customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. + operationId: agentGetExternalAccount tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/InvitationClaimedWebhook' - examples: - claimedInvitation: - summary: Invitation claimed notification - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: INVITATION.CLAIMED - timestamp: '2025-09-01T15:45:00Z' - data: - code: 019542f5 - createdAt: '2025-09-01T14:30:00Z' - claimedAt: '2025-09-01T15:45:00Z' - inviterUma: $inviter@uma.domain - inviteeUma: $invitee@uma.domain - status: CLAIMED - url: https://uma.me/i/019542f5 + - AgentAuth: [] responses: '200': - description: Webhook received successfully - '400': - description: Bad request + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/ExternalAccount' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + '404': + description: External account not found content: application/json: schema: - $ref: '#/components/schemas/Error409' - customer-update: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + delete: + summary: Delete agent external account + description: | + Delete an external account belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. + operationId: agentDeleteExternalAccount + tags: + - Agent Operations + security: + - AgentAuth: [] + responses: + '204': + description: External account deleted successfully + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: External account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /cards: post: - summary: Customer status change + summary: Issue a card description: | - Webhook that is called when the status of a customer is updated, including KYC and KYB status changes. - This endpoint should be implemented by clients of the Grid API. + Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`. - ### 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 API 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 any funding source is an Embedded Wallet internal account, the cardholder must authorize Grid to sign Spark token transactions for that card funding source by completing the delegated-key creation flow with `POST /auth/delegated-keys`. Until an active delegated key exists for that funding source, Authorization Decisioning cannot use it to fund card transactions. - If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. - operationId: customerStatusWebhook + New cards start in `state: "PROCESSING"` while the card issuer provisions the card. The `card.state_change` webhook fires on each state transition, including the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails). + operationId: createCard tags: - - Webhooks + - Cards security: - - WebhookSignature: [] + - BasicAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CustomerWebhook' + $ref: '#/components/schemas/CardCreateRequest' examples: - kycApprovedWebhook: - summary: When an individual customer KYC has been approved + virtualCard: + summary: Issue a virtual card with one funding source + value: + cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCardId: card-emp-aary-001 + form: VIRTUAL + fundingSources: + - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + responses: + '201': + description: Card created successfully. Newly-created cards start in `PROCESSING` while the issuer provisions them. Cards funded by an Embedded Wallet internal account also require an active delegated key for that funding source before Authorization Decisioning can use it. + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + '400': + description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Card issuance is not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + get: + summary: List cards + description: | + Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller. + operationId: listCards + tags: + - Cards + security: + - BasicAuth: [] + parameters: + - name: cardholderId + in: query + description: Filter by cardholder (customer) id. + required: false + schema: + type: string + - name: accountId + in: query + description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id. + required: false + schema: + type: string + - name: platformCardId + in: query + description: Filter by platform-specific card identifier. + required: false + schema: + type: string + - name: state + in: query + description: Filter by card state. + required: false + schema: + $ref: '#/components/schemas/CardState' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/CardListResponse' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /cards/{id}: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + get: + summary: Get a card + description: Retrieve a card by its system-generated id. To display the card's full PAN, CVV, and expiry to the cardholder, request a reveal with `POST /cards/{id}/reveal` — the card resource itself never carries the reveal URL. + operationId: getCardById + tags: + - Cards + security: + - BasicAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + patch: + summary: Update a card + description: | + Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied. + + - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`. + - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`. + + Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`): + + 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`. + + Effects: + - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed. + - `state: ACTIVE`: normal authorization behavior resumes. + - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys. + - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding. + + The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated. + operationId: updateCardById + tags: + - Cards + security: + - BasicAuth: [] + parameters: + - name: Grid-Wallet-Signature + in: header + required: false + description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CardUpdateRequest' + examples: + freeze: + summary: Freeze an active card + value: + state: FROZEN + unfreeze: + summary: Unfreeze a frozen card + value: + state: ACTIVE + updateFundingSources: + summary: Replace the card's bound funding sources + value: + fundingSources: + - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + - InternalAccount:019542f5-b3e7-1d02-0000-000000000003 + freezeAndUpdateSources: + summary: Freeze the card and replace its funding sources in one call + value: + state: FROZEN + fundingSources: + - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + close: + summary: Permanently close the card + value: + state: CLOSED + responses: + '200': + description: Signed retry accepted. Returns the updated card. + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + '202': + description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/SignedRequestChallenge' + '400': + description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge. + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /cards/{id}/reveal: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + post: + summary: Reveal card details + description: |- + Mint a signed, short-lived URL for the card processor's iframe that displays the card's full PAN, CVV, and expiry to the cardholder. This is the only way to obtain a reveal URL — the `Card` resource never carries one. + + Request the reveal right before rendering the iframe and render the returned `panEmbedUrl` immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log the URL — it is a bearer secret for the full card details. The card data renders inside the processor's iframe and never crosses Grid's or your servers. + + Every reveal is audit-logged with the requesting actor. + operationId: revealCard + tags: + - Cards + security: + - BasicAuth: [] + responses: + '200': + description: Reveal URL minted. + content: + application/json: + schema: + $ref: '#/components/schemas/CardRevealResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden. The session has no attributable actor to audit the reveal against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /sandbox/cards/{id}/simulate/authorization: + post: + summary: Simulate a card authorization + description: | + Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip. + + The decisioning outcome is controlled by the last three characters of `merchant.descriptor`: + + | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved | + + Production returns `404` on this path. + operationId: sandboxSimulateCardAuthorization + tags: + - Sandbox + security: + - BasicAuth: [] + parameters: + - name: id + in: path + required: true + description: The id of the card to simulate an authorization against. + schema: + type: string + example: Card:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxCardAuthorizationRequest' + examples: + coffeeAuth: + summary: Approved $12.50 auth at a coffee shop + value: + amount: 1250 + currency: + code: USD + merchant: + descriptor: BLUE BOTTLE COFFEE SF + mcc: '5814' + country: US + declinedInsufficientFunds: + summary: Declined — insufficient funds (descriptor suffix `002`) + value: + amount: 50000 + currency: + code: USD + merchant: + descriptor: AMAZON RETAIL US-002 + mcc: '5942' + country: US + responses: + '200': + description: Simulated authorization processed. Returns the resulting card transaction. + content: + application/json: + schema: + $ref: '#/components/schemas/CardTransaction' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found (also returned in production for this path) + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /sandbox/cards/{id}/simulate/clearing: + post: + summary: Simulate a card clearing + description: | + Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment. + + - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth). + - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted. + - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases). + + Production returns `404` on this path. + operationId: sandboxSimulateCardClearing + tags: + - Sandbox + security: + - BasicAuth: [] + parameters: + - name: id + in: path + required: true + description: The id of the card the clearing applies to. + schema: + type: string + example: Card:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxCardClearingRequest' + examples: + tipOnTopClearing: + summary: Clearing larger than auth — exercises post-hoc pull + value: + cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 + amount: 1500 + authorizationExpiry: + summary: Clearing of 0 — exercises authorization expiry + value: + cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 + amount: 0 + responses: + '200': + description: Simulated clearing processed. Returns the updated card transaction. + content: + application/json: + schema: + $ref: '#/components/schemas/CardTransaction' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card or card transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /sandbox/cards/{id}/simulate/return: + post: + summary: Simulate a card return + description: | + Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund). + + Production returns `404` on this path. + operationId: sandboxSimulateCardReturn + tags: + - Sandbox + security: + - BasicAuth: [] + parameters: + - name: id + in: path + required: true + description: The id of the card the return applies to. + schema: + type: string + example: Card:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxCardReturnRequest' + examples: + fullRefund: + summary: Full refund of a $15.00 settled transaction + value: + cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 + amount: 1500 + responses: + '200': + description: Simulated return processed. Returns the updated card transaction. + content: + application/json: + schema: + $ref: '#/components/schemas/CardTransaction' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card or card transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /stablecoin-provider-accounts: + post: + summary: Link a stablecoin provider account + description: | + Link provider API credentials for the authenticated platform. Provider credentials are account-scoped and can be reused for multiple stablecoins. Currently, the only supported provider is `BRALE`; the provider environment is derived from the authenticated Grid platform mode. + operationId: linkStablecoinProviderAccount + tags: + - Stablecoins + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + description: Idempotency key for retrying this request safely. Replays return the prior result; conflicting payloads are rejected. + required: true + schema: + type: string + maxLength: 255 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StablecoinProviderAccountLinkRequest' + responses: + '201': + description: Stablecoin provider account linked + content: + application/json: + schema: + $ref: '#/components/schemas/StablecoinProviderAccount' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '409': + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + get: + summary: List stablecoin provider account links + description: Retrieve stablecoin provider account links for the authenticated platform. + operationId: listStablecoinProviderAccounts + tags: + - Stablecoins + security: + - BasicAuth: [] + parameters: + - name: provider + in: query + required: false + schema: + $ref: '#/components/schemas/StablecoinProvider' + - name: status + in: query + required: false + schema: + $ref: '#/components/schemas/StablecoinProviderAccountStatus' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/StablecoinProviderAccountListResponse' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /stablecoin-provider-accounts/{stablecoinProviderAccountId}: + parameters: + - name: stablecoinProviderAccountId + in: path + description: System-generated stablecoin provider account link identifier + required: true + schema: + type: string + get: + summary: Get a stablecoin provider account link + description: Retrieve a stablecoin provider account link by id for the authenticated platform. + operationId: getStablecoinProviderAccount + tags: + - Stablecoins + security: + - BasicAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/StablecoinProviderAccount' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Stablecoin provider account link not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' +webhooks: + agent-action: + post: + summary: Agent action pending approval webhook + description: | + Fired when an agent submits an action that requires platform approval before Grid will execute it. Use this to send a push notification to the customer so they can review and approve or reject the action in your app. + 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. + + The payload contains the full `AgentAction` — including the embedded quote or transfer details — so you can render the approval UI without a second API call. Approve or reject via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`. + operationId: agentActionWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AgentActionWebhook' + examples: + pendingApproval: + summary: Agent action pending approval + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000020 + type: AGENT_ACTION.PENDING_APPROVAL + timestamp: '2025-10-03T15:00:00Z' + data: + id: AgentAction:019542f5-b3e7-1d02-0000-000000000099 + agentId: Agent:019542f5-b3e7-1d02-0000-000000000042 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000010 + platformCustomerId: user-a1b2c3 + status: PENDING_APPROVAL + type: EXECUTE_QUOTE + quote: + id: Quote:019542f5-b3e7-1d02-0000-000000000006 + status: PENDING + expiresAt: '2025-10-03T15:00:30Z' + createdAt: '2025-10-03T15:00:00Z' + source: + sourceType: ACCOUNT + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + sendingCurrency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + receivingCurrency: + code: INR + name: Indian Rupee + symbol: ₹ + decimals: 2 + totalSendingAmount: 50000 + totalReceivingAmount: 4625000 + exchangeRate: 92.5 + feesIncluded: 250 + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000099 + createdAt: '2025-10-03T15:00:00Z' + updatedAt: '2025-10-03T15:00:00Z' + responses: + '200': + description: Webhook received and acknowledged. + '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' + incoming-payment: + post: + summary: Incoming payment webhook and approval mechanism + description: | + Webhook that is called when an incoming payment is received by a customer's UMA address. + 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. + + ### Payment Approval Flow + When a transaction has `status: "PENDING"`, this webhook serves as an approval mechanism: + + 1. The client should check the `counterpartyInformation` against their requirements + 2. To APPROVE the payment synchronously, return a 200 OK response + 3. To REJECT the payment, return a 403 Forbidden response with an Error object + 4. To request more information, return a 422 Unprocessable Entity with specific missing fields + 5. To process the payment asynchronously, return a 202 Accepted response and then call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint within 5 seconds. Note that synchronous approval/rejection is preferred where possible. + + The Grid system will proceed or cancel the payment based on your response. + + For transactions with other statuses (COMPLETED, FAILED, REFUNDED), this webhook is purely informational. + operationId: incomingPaymentWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/IncomingPaymentWebhook' + examples: + pendingPayment: + summary: Pending payment example requiring approval + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.PENDING + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000005 + status: PENDING + type: INCOMING + direction: CREDIT + destination: + destinationType: UMA_ADDRESS + umaAddress: $recipient@uma.domain + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + senderUmaAddress: $sender@external.domain + receiverUmaAddress: $recipient@uma.domain + receivedAmount: + amount: 50000 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + counterpartyInformation: + FULL_NAME: John Sender + BIRTH_DATE: '1985-06-15' + NATIONALITY: US + reconciliationInstructions: + reference: REF-123456789 + requestedReceiverCustomerInfoFields: + - name: NATIONALITY + mandatory: true + - name: POSTAL_ADDRESS + mandatory: false + incomingCompletedPayment: + summary: Completed payment notification + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000005 + status: COMPLETED + type: INCOMING + direction: CREDIT + destination: + destinationType: UMA_ADDRESS + umaAddress: $recipient@uma.domain + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + senderUmaAddress: $sender@external.domain + receiverUmaAddress: $recipient@uma.domain + receivedAmount: + amount: 50000 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: Payment for services + reconciliationInstructions: + reference: REF-123456789 + incomingCompletedCryptoPayment: + summary: Completed payment funded from an external crypto wallet + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000009 + type: INCOMING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000006 + status: COMPLETED + type: INCOMING + direction: CREDIT + source: + sourceType: REALTIME_FUNDING + currency: USDC + onChainTransaction: + transactionHash: 7RJWhvQBQPEjJmki5fhBboGBWRJhmcFkMvrr4Fu3tMSJ5EdynMEiYSyiWAH9GpcbHpeUzeSQF9ZY6q4x8AhBskUf + network: SOLANA + destination: + destinationType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + receivedAmount: + amount: 100000 + currency: + code: USDC + name: USD Coin + symbol: '' + decimals: 6 + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: USDC deposit from self-custody wallet + responses: + '200': + description: | + Webhook received successfully. + For PENDING transactions, this indicates approval to proceed with the payment. + If `requestedReceiverCustomerInfoFields` were present in the webhook request, the corresponding fields for the recipient must be included in this response in the `receiverCustomerInfo` object. + content: + application/json: + schema: + $ref: '#/components/schemas/IncomingPaymentWebhookResponse' + '202': + description: | + Webhook received and will be processed asynchronously. The synchronous 200 response should be preferred where possible. This asycnhronous path should only be used in + cases where the platform's architecture requires async (but still very quick) processing before approving or rejecting the payment. + The platform must call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint to approve or reject the payment within 5 seconds or the payment will be automatically rejected. + '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' + '403': + description: | + Forbidden - Payment rejected by the client. + Only applicable for PENDING transactions. + content: + application/json: + schema: + $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse' + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '422': + description: | + Unprocessable Entity - Additional counterparty information required. + Only applicable for PENDING transactions. + content: + application/json: + schema: + $ref: '#/components/schemas/IncomingPaymentWebhookUnprocessableResponse' + outgoing-payment: + post: + summary: Outgoing payment status webhook + description: | + Webhook that is called when an outgoing payment's status changes. + 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. + + This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded. + operationId: outgoingPaymentWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OutgoingPaymentWebhook' + examples: + outgoingCompletedPayment: + summary: Completed outgoing payment + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000005 + status: COMPLETED + type: OUTGOING + direction: DEBIT + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + senderUmaAddress: $sender@uma.domain + receiverUmaAddress: $recipient@external.domain + sentAmount: + amount: 10550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + receivedAmount: + amount: 9706 + currency: + code: EUR + name: Euro + symbol: € + decimals: 2 + exchangeRate: 0.92 + quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: 'Payment for invoice #1234' + paymentInstructions: [] + rateDetails: + counterpartyMultiplier: 1.08 + counterpartyFixedFee: 10 + gridApiMultiplier: 0.925 + gridApiFixedFee: 10 + gridApiVariableFeeRate: 0.003 + gridApiVariableFeeAmount: 30 + outgoingCompletedCryptoPayment: + summary: Completed crypto payout to an external wallet + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: OUTGOING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000009 + status: COMPLETED + type: OUTGOING + direction: DEBIT + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:c34dcbd6-dced-4ec4-b756-3c3a9ea3d789 + onChainTransaction: + transactionHash: h82pJGF9p7kpzb6eU326EFZf2cDnimbTFVeJtx1qtBmUNJAEqN76R7PwPfHt3oWb8R6cKvhgyxQdDn53jFrK6wFx + network: SOLANA + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + sentAmount: + amount: 100000 + currency: + code: USDC + name: USD Coin + symbol: '' + decimals: 6 + receivedAmount: + amount: 100000 + currency: + code: USDC + name: USD Coin + symbol: '' + decimals: 6 + quoteId: Quote:019542f5-b3e7-1d02-0000-000000000010 + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: USDC withdrawal to self-custody wallet + paymentInstructions: [] + failedPayment: + summary: Failed outgoing payment + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000005 + status: FAILED + type: OUTGOING + direction: DEBIT + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + senderUmaAddress: $sender@uma.domain + receiverUmaAddress: $recipient@external.domain + sentAmount: + amount: 10550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + createdAt: '2025-08-15T14:25:18Z' + quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 + failureReason: QUOTE_EXECUTION_FAILED + 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' + test-webhook: + post: + summary: Test webhook for integration verification + description: | + Webhook that is sent once to verify your webhook endpoint is correctly set up. + This is sent when you configure or update your platform settings with a webhook URL. + + ### Authentication + The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by the Grid API. + 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. + + This webhook is purely for testing your endpoint integration and signature verification. + operationId: testWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TestWebhookRequest' + examples: + testWebhook: + summary: Test webhook example + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000001 + type: TEST + timestamp: '2025-08-15T14:32:00Z' + data: {} + responses: + '200': + description: Webhook received successfully. This confirms your webhook endpoint is properly configured. + '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' + bulk-upload: + post: + summary: Bulk upload status webhook + description: | + Webhook that is called when a bulk customer upload job completes or fails. + 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. + + This webhook is sent when a bulk upload job completes or fails, providing detailed information about the results. + operationId: bulkUploadWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BulkUploadWebhook' + examples: + completedUpload: + summary: Successful bulk upload completion + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Job:019542f5-b3e7-1d02-0000-000000000006 + status: COMPLETED + progress: + total: 5000 + processed: 5000 + successful: 5000 + failed: 0 + errors: [] + failedUpload: + summary: Failed bulk upload + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Job:019542f5-b3e7-1d02-0000-000000000006 + status: FAILED + progress: + total: 5000 + processed: 5000 + successful: 0 + failed: 5000 + errors: + - correlationId: row_1 + error: + code: invalid_csv_format + message: Invalid CSV format + details: + reason: missing_required_column + column: umaAddress + 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' + invitation-claimed: + post: + summary: Invitation claimed webhook + description: | + Webhook that is called when an invitation is claimed by a customer. + This endpoint should be implemented by platform clients of the Grid API. + + When a customer claims an invitation, this webhook is triggered to notify the platform that: + 1. The invitation has been successfully claimed + 2. The invitee UMA address is now associated with the invitation + 3. The invitation status has changed from PENDING to CLAIMED + + This allows platforms to: + - Track invitation usage and conversion rates + - Trigger onboarding flows for new customers who joined via invitation + - Apply referral bonuses or rewards to the inviter + - Update their UI to reflect the claimed status + + ### 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. + operationId: invitationClaimedWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InvitationClaimedWebhook' + examples: + claimedInvitation: + summary: Invitation claimed notification + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: INVITATION.CLAIMED + timestamp: '2025-09-01T15:45:00Z' + data: + code: 019542f5 + createdAt: '2025-09-01T14:30:00Z' + claimedAt: '2025-09-01T15:45:00Z' + inviterUma: $inviter@uma.domain + inviteeUma: $invitee@uma.domain + status: CLAIMED + url: https://uma.me/i/019542f5 + 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' + customer-update: + post: + summary: Customer status change + description: | + Webhook that is called when the status of a customer is updated, including KYC and KYB status changes. + 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 API 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. + operationId: customerStatusWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CustomerWebhook' + examples: + kycApprovedWebhook: + summary: When an individual customer KYC has been approved value: id: Webhook:019542f5-b3e7-1d02-0000-000000000007 type: CUSTOMER.KYC_APPROVED @@ -10110,7 +11018,131 @@ components: example: OPERATING_REVENUE BusinessInfoUpdate: type: object - description: Additional information for business entities + description: Additional information for business entities + properties: + legalName: + type: string + description: Legal name of the business + example: Acme Corporation, Inc. + doingBusinessAs: + type: string + description: Trade name or DBA name of the business, if different from the legal name + example: Acme + country: + type: string + description: Country of incorporation or registration (ISO 3166-1 alpha-2) + example: US + registrationNumber: + type: string + description: Business registration number + example: '5523041' + incorporatedOn: + type: string + format: date + description: Date of incorporation in ISO 8601 format (YYYY-MM-DD) + example: '2018-03-14' + entityType: + $ref: '#/components/schemas/EntityType' + taxId: + type: string + description: Tax identification number + example: 47-1234567 + countriesOfOperation: + type: array + items: + type: string + description: List of countries where the business operates (ISO 3166-1 alpha-2) + example: + - US + businessType: + $ref: '#/components/schemas/BusinessType' + purposeOfAccount: + $ref: '#/components/schemas/PurposeOfAccount' + sourceOfFunds: + type: string + description: The primary source of funds for the business + example: Funds derived from customer payments for software services + expectedMonthlyTransactionCount: + type: string + enum: + - COUNT_UNDER_10 + - COUNT_10_TO_100 + - COUNT_100_TO_500 + - COUNT_500_TO_1000 + - COUNT_OVER_1000 + description: Expected number of transactions per month + example: COUNT_100_TO_500 + expectedMonthlyTransactionVolume: + type: string + enum: + - VOLUME_UNDER_10K + - VOLUME_10K_TO_100K + - VOLUME_100K_TO_1M + - VOLUME_1M_TO_10M + - VOLUME_OVER_10M + description: Expected total transaction volume per month in USD equivalent + example: VOLUME_100K_TO_1M + expectedRecipientJurisdictions: + type: array + items: + type: string + description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2) + example: + - US + naicsCode: + type: string + pattern: ^\d{2,6}$ + description: NAICS code describing the nature of the business (2-6 digits) + example: '541511' + sourceOfFundsCategories: + type: array + items: + $ref: '#/components/schemas/SourceOfFundsCategory' + description: Structured source-of-funds categories for the business + example: + - OPERATING_REVENUE + sourceOfFundsOtherDescription: + type: string + minLength: 1 + maxLength: 500 + description: Description of the source of funds when OTHER is selected + example: Proceeds from a legal settlement + purposeOfAccountOtherDescription: + type: string + minLength: 1 + maxLength: 500 + description: Description of the account purpose when OTHER is selected + example: Escrow for equipment leases + expectedCounterpartyCountries: + type: array + items: + type: string + description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) + example: + - US + BusinessCustomerFields: + type: object + required: + - customerType + properties: + customerType: + type: string + enum: + - BUSINESS + kybStatus: + $ref: '#/components/schemas/KybStatus' + address: + $ref: '#/components/schemas/Address' + businessInfo: + $ref: '#/components/schemas/BusinessInfoUpdate' + BusinessInfoResponse: + type: object + description: | + Business information returned on a customer. `taxId` and `incorporatedOn` are + required on creation but may be absent on legacy customers that pre-date the + requirement, so both are optional in responses. + required: + - legalName properties: legalName: type: string @@ -10157,84 +11189,309 @@ components: expectedMonthlyTransactionCount: type: string enum: - - COUNT_UNDER_10 - - COUNT_10_TO_100 - - COUNT_100_TO_500 - - COUNT_500_TO_1000 - - COUNT_OVER_1000 - description: Expected number of transactions per month - example: COUNT_100_TO_500 - expectedMonthlyTransactionVolume: + - COUNT_UNDER_10 + - COUNT_10_TO_100 + - COUNT_100_TO_500 + - COUNT_500_TO_1000 + - COUNT_OVER_1000 + description: Expected number of transactions per month + example: COUNT_100_TO_500 + expectedMonthlyTransactionVolume: + type: string + enum: + - VOLUME_UNDER_10K + - VOLUME_10K_TO_100K + - VOLUME_100K_TO_1M + - VOLUME_1M_TO_10M + - VOLUME_OVER_10M + description: Expected total transaction volume per month in USD equivalent + example: VOLUME_100K_TO_1M + expectedRecipientJurisdictions: + type: array + items: + type: string + description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2) + example: + - US + naicsCode: + type: string + pattern: ^\d{2,6}$ + description: NAICS code describing the nature of the business (2-6 digits) + example: '541511' + sourceOfFundsCategories: + type: array + items: + $ref: '#/components/schemas/SourceOfFundsCategory' + description: Structured source-of-funds categories for the business + example: + - OPERATING_REVENUE + sourceOfFundsOtherDescription: + type: string + minLength: 1 + maxLength: 500 + description: Description of the source of funds when OTHER is selected + example: Proceeds from a legal settlement + purposeOfAccountOtherDescription: + type: string + minLength: 1 + maxLength: 500 + description: Description of the account purpose when OTHER is selected + example: Escrow for equipment leases + expectedCounterpartyCountries: + type: array + items: + type: string + description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) + example: + - US + BeneficialOwnerRole: + type: string + enum: + - UBO + - DIRECTOR + - COMPANY_OFFICER + - CONTROL_PERSON + - TRUSTEE + - GENERAL_PARTNER + description: Role of the beneficial owner within the business + example: UBO + IdentificationType: + type: string + enum: + - SSN + - ITIN + - EIN + - NON_US_TAX_ID + description: Type of tax identification + example: SSN + BeneficialOwnerPersonalInfo: + type: object + required: + - firstName + - lastName + - birthDate + - nationality + - address + - idType + - identifier + properties: + firstName: + type: string + description: First name of the individual + example: Jane + middleName: + type: string + description: Middle name of the individual + example: Marie + lastName: + type: string + description: Last name of the individual + example: Smith + birthDate: + type: string + format: date + description: Date of birth in ISO 8601 format (YYYY-MM-DD) + example: '1978-06-15' + nationality: + type: string + description: Country of nationality (ISO 3166-1 alpha-2) + example: US + email: + type: string + format: email + description: Email address of the individual + example: jane.smith@acmecorp.com + phoneNumber: + type: string + description: Phone number in E.164 format + example: '+14155550192' + pattern: ^\+[1-9]\d{1,14}$ + address: + $ref: '#/components/schemas/Address' + idType: + $ref: '#/components/schemas/IdentificationType' + identifier: + type: string + description: The identification number or value + example: 123-45-6789 + countryOfIssuance: + type: string + description: Country that issued the identification (ISO 3166-1 alpha-2) + example: US + BeneficialOwner: + type: object + required: + - id + - customerId + - roles + - ownershipPercentage + - personalInfo + - kycStatus + - createdAt + properties: + id: + type: string + description: Unique identifier for this beneficial owner + example: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000001 + customerId: + type: string + description: The ID of the business customer this beneficial owner is associated with + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + roles: + type: array + items: + $ref: '#/components/schemas/BeneficialOwnerRole' + description: Roles of this person within the business + example: + - UBO + - DIRECTOR + ownershipPercentage: + type: integer + description: Percentage of ownership in the business (0-100) + minimum: 0 + maximum: 100 + example: 51 + personalInfo: + $ref: '#/components/schemas/BeneficialOwnerPersonalInfo' + kycStatus: + $ref: '#/components/schemas/KycStatus' + createdAt: + type: string + format: date-time + description: When this beneficial owner was created + example: '2025-10-03T12:00:00Z' + updatedAt: + type: string + format: date-time + description: When this beneficial owner was last updated + example: '2025-10-03T12:00:00Z' + BusinessCustomer: + title: Business Customer + allOf: + - $ref: '#/components/schemas/Customer' + - $ref: '#/components/schemas/BusinessCustomerFields' + - type: object + properties: + businessInfo: + $ref: '#/components/schemas/BusinessInfoResponse' + beneficialOwners: + type: array + items: + $ref: '#/components/schemas/BeneficialOwner' + CustomerOneOf: + oneOf: + - $ref: '#/components/schemas/IndividualCustomer' + - $ref: '#/components/schemas/BusinessCustomer' + discriminator: + propertyName: customerType + mapping: + INDIVIDUAL: '#/components/schemas/IndividualCustomer' + BUSINESS: '#/components/schemas/BusinessCustomer' + CustomerListResponse: + type: object + required: + - data + - hasMore + properties: + data: + type: array + description: List of customers matching the filter criteria + items: + $ref: '#/components/schemas/CustomerOneOf' + 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) + Error405: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 405 + description: HTTP status code + code: type: string + description: | + | Error Code | Description | + |------------|-------------| + | METHOD_NOT_ALLOWED | The HTTP method is not supported for this endpoint | enum: - - VOLUME_UNDER_10K - - VOLUME_10K_TO_100K - - VOLUME_100K_TO_1M - - VOLUME_1M_TO_10M - - VOLUME_OVER_10M - description: Expected total transaction volume per month in USD equivalent - example: VOLUME_100K_TO_1M - expectedRecipientJurisdictions: - type: array - items: - type: string - description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2) - example: - - US - naicsCode: + - METHOD_NOT_ALLOWED + message: type: string - pattern: ^\d{2,6}$ - description: NAICS code describing the nature of the business (2-6 digits) - example: '541511' - sourceOfFundsCategories: - type: array - items: - $ref: '#/components/schemas/SourceOfFundsCategory' - description: Structured source-of-funds categories for the business - example: - - OPERATING_REVENUE - sourceOfFundsOtherDescription: + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + CustomerCreateRequest: + type: object + required: + - customerType + properties: + platformCustomerId: type: string - minLength: 1 - maxLength: 500 - description: Description of the source of funds when OTHER is selected - example: Proceeds from a legal settlement - purposeOfAccountOtherDescription: + description: Platform-specific customer identifier. If not provided, one will be generated by the system. + example: 9f84e0c2a72c4fa + customerType: + $ref: '#/components/schemas/CustomerType' + region: type: string - minLength: 1 - maxLength: 500 - description: Description of the account purpose when OTHER is selected - example: Escrow for equipment leases - expectedCounterpartyCountries: + description: Country code (ISO 3166-1 alpha-2) representing the customer's regional identity. This determines the regulatory jurisdiction and KYC requirements for the customer. Required if the customer will use currencies with different KYC requirements across regions. A customer with accounts in multiple regions should be registered as separate customers. This field is immutable after creation. + example: US + currencies: type: array items: type: string - description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) + description: List of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Required if the customer will use more than one sending currency, since the correct currencies cannot always be inferred. If not provided, currencies will be inferred from the customer's region. Some currency combinations may require separate customers — if so, the request will be rejected with details. example: - - US - BusinessCustomerFields: - type: object - required: - - customerType - properties: - customerType: + - USD + - USDC + email: type: string - enum: - - BUSINESS - kybStatus: - $ref: '#/components/schemas/KybStatus' - address: - $ref: '#/components/schemas/Address' - businessInfo: - $ref: '#/components/schemas/BusinessInfoUpdate' - BusinessInfoResponse: + format: email + description: Email address for the customer. + example: john.doe@example.com + phoneNumber: + type: string + pattern: ^\+[1-9]\d{1,14}$ + description: Phone number for the customer in strict E.164 format. + example: '+14155551234' + umaAddress: + type: string + description: Optional UMA address identifier. If not provided during customer creation, one will be generated by the system. If provided during customer update, the UMA address will be updated to the provided value. This is an optional identifier to route payments to the customer. This is an optional identifier to route payments to the customer. + example: $john.doe@uma.domain.com + IndividualCustomerCreateRequest: + title: Individual Customer Create Request + allOf: + - $ref: '#/components/schemas/CustomerCreateRequest' + - $ref: '#/components/schemas/IndividualCustomerFields' + - type: object + properties: + idType: + $ref: '#/components/schemas/IdentificationType' + identifier: + type: string + writeOnly: true + description: The individual's tax identification number. Required to onboard the individual as a US account holder. Only SSN and ITIN are currently accepted for an individual account holder; other identification types are rejected. Write-only — never returned in customer responses. + example: 123-45-6789 + BusinessInfo: type: object - description: | - Business information returned on a customer. `taxId` and `incorporatedOn` are - required on creation but may be absent on legacy customers that pre-date the - requirement, so both are optional in responses. + description: Additional information required for business entities required: - legalName + - taxId + - incorporatedOn properties: legalName: type: string @@ -10336,454 +11593,621 @@ components: description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) example: - US - BeneficialOwnerRole: - type: string - enum: - - UBO - - DIRECTOR - - COMPANY_OFFICER - - CONTROL_PERSON - - TRUSTEE - - GENERAL_PARTNER - description: Role of the beneficial owner within the business - example: UBO - IdentificationType: - type: string - enum: - - SSN - - ITIN - - EIN - - NON_US_TAX_ID - description: Type of tax identification - example: SSN - BeneficialOwnerPersonalInfo: + BusinessCustomerCreateRequest: + title: Business Customer Create Request + allOf: + - $ref: '#/components/schemas/CustomerCreateRequest' + - $ref: '#/components/schemas/BusinessCustomerFields' + - type: object + properties: + businessInfo: + $ref: '#/components/schemas/BusinessInfo' + CustomerCreateRequestOneOf: + oneOf: + - $ref: '#/components/schemas/IndividualCustomerCreateRequest' + - $ref: '#/components/schemas/BusinessCustomerCreateRequest' + discriminator: + propertyName: customerType + mapping: + INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest' + BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest' + Error409: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 409 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL | Transaction is not pending platform approval | + | UMA_ADDRESS_EXISTS | UMA address already exists | + | EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential | + | EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued | + | PASSKEY_ALREADY_ENROLLED | The customer already has an enrolled passkey factor; only one passkey per customer is supported. Delete the existing one before enrolling another | + | CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform | + enum: + - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL + - UMA_ADDRESS_EXISTS + - EMAIL_OTP_EMAIL_ALREADY_EXISTS + - EMAIL_OTP_CREDENTIAL_SET_CHANGED + - PASSKEY_ALREADY_ENROLLED + - CONFLICT + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + Error404: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 404 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | TRANSACTION_NOT_FOUND | Transaction not found | + | INVITATION_NOT_FOUND | Invitation not found | + | USER_NOT_FOUND | Customer not found | + | QUOTE_NOT_FOUND | Quote not found | + | LOOKUP_REQUEST_NOT_FOUND | Lookup request not found | + | TOKEN_NOT_FOUND | Token not found | + | BULK_UPLOAD_JOB_NOT_FOUND | Bulk upload job not found | + | REFERENCE_NOT_FOUND | Reference not found | + | UMA_NOT_FOUND | The UMA address is well-formed but no receiver exists at the counterparty VASP | + | STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND | Stablecoin provider account link not found | + enum: + - TRANSACTION_NOT_FOUND + - INVITATION_NOT_FOUND + - USER_NOT_FOUND + - QUOTE_NOT_FOUND + - LOOKUP_REQUEST_NOT_FOUND + - TOKEN_NOT_FOUND + - BULK_UPLOAD_JOB_NOT_FOUND + - REFERENCE_NOT_FOUND + - UMA_NOT_FOUND + - STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + Error410: type: object required: - - firstName - - lastName - - birthDate - - nationality - - address - - idType - - identifier + - message + - status + - code properties: - firstName: - type: string - description: First name of the individual - example: Jane - middleName: - type: string - description: Middle name of the individual - example: Marie - lastName: - type: string - description: Last name of the individual - example: Smith - birthDate: + status: + type: integer + enum: + - 410 + description: HTTP status code + code: type: string - format: date - description: Date of birth in ISO 8601 format (YYYY-MM-DD) - example: '1978-06-15' - nationality: + description: | + | Error Code | Description | + |------------|-------------| + | CUSTOMER_DELETED | Customer has been permanently deleted | + enum: + - CUSTOMER_DELETED + message: type: string - description: Country of nationality (ISO 3166-1 alpha-2) - example: US + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + CustomerUpdateRequest: + title: Customer Update Request + description: Request body for `PATCH /customers/{customerId}`. When `email` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer email and every tied `EMAIL_OTP` credential through the endpoint's signed-retry flow. When `phoneNumber` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer phone number and every tied `SMS_OTP` credential through the same signed-retry flow. Update `email` and `phoneNumber` in separate PATCH calls. + type: object + required: + - customerType + properties: + customerType: + $ref: '#/components/schemas/CustomerType' + currencies: + type: array + items: + type: string + description: Updated list of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Replaces the existing list. Some currency combinations may require separate customers — if so, the request will be rejected with details. + example: + - USD + - EUR + - USDC email: type: string format: email - description: Email address of the individual - example: jane.smith@acmecorp.com + description: Email address for the customer. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `EMAIL_OTP` credential across all tied Embedded Wallets. + example: john.doe@example.com phoneNumber: type: string - description: Phone number in E.164 format - example: '+14155550192' pattern: ^\+[1-9]\d{1,14}$ - address: - $ref: '#/components/schemas/Address' - idType: - $ref: '#/components/schemas/IdentificationType' - identifier: - type: string - description: The identification number or value - example: 123-45-6789 - countryOfIssuance: + description: Phone number for the customer in strict E.164 format. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `SMS_OTP` credential across all tied Embedded Wallets. Send phone number and email updates as separate PATCH calls. + example: '+14155551234' + umaAddress: type: string - description: Country that issued the identification (ISO 3166-1 alpha-2) - example: US - BeneficialOwner: + description: Optional UMA address identifier. If provided, the customer's UMA address will be updated. This is an optional identifier to route payments to the customer. + example: $john.doe@uma.domain.com + IndividualCustomerUpdateRequest: + title: Individual Customer Update Request + allOf: + - $ref: '#/components/schemas/CustomerUpdateRequest' + - $ref: '#/components/schemas/IndividualCustomerFields' + BusinessCustomerUpdateRequest: + title: Business Customer Update Request + allOf: + - $ref: '#/components/schemas/CustomerUpdateRequest' + - $ref: '#/components/schemas/BusinessCustomerFields' + CustomerUpdateRequestOneOf: + oneOf: + - $ref: '#/components/schemas/IndividualCustomerUpdateRequest' + - $ref: '#/components/schemas/BusinessCustomerUpdateRequest' + discriminator: + propertyName: customerType + mapping: + INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' + BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' + SignedRequestChallenge: + title: Signed Request Challenge type: object required: - - id - - customerId - - roles - - ownershipPercentage - - personalInfo - - kycStatus - - createdAt + - payloadToSign + - requestId + - expiresAt + description: Common base for two-step signed-retry challenge responses on Embedded Wallet endpoints (credential registration or revocation, session refresh or revocation, wallet export, customer email updates, and similar). Holds the signing fields shared across every challenge shape; each variant composes this base via `allOf` and adds its own resource `id` (and `type`, when applicable) with variant-specific description and example. properties: - id: + payloadToSign: type: string - description: Unique identifier for this beneficial owner - example: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000001 - customerId: + description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request. + example: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_EXAMPLE"}' + requestId: type: string - description: The ID of the business customer this beneficial owner is associated with - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - roles: - type: array - items: - $ref: '#/components/schemas/BeneficialOwnerRole' - description: Roles of this person within the business - example: - - UBO - - DIRECTOR - ownershipPercentage: - type: integer - description: Percentage of ownership in the business (0-100) - minimum: 0 - maximum: 100 - example: 51 - personalInfo: - $ref: '#/components/schemas/BeneficialOwnerPersonalInfo' - kycStatus: - $ref: '#/components/schemas/KycStatus' - createdAt: + description: Grid-issued `Request:` identifier for this pending request. Echo this value exactly in the `Request-Id` header on the signed retry so the server can correlate the retry with the issued challenge. + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: type: string format: date-time - description: When this beneficial owner was created - example: '2025-10-03T12:00:00Z' - updatedAt: + description: Timestamp after which this challenge is no longer valid. The signed retry must be submitted before this time. + example: '2026-04-08T15:35:00Z' + Error424: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 424 + description: HTTP status code + code: type: string - format: date-time - description: When this beneficial owner was last updated - example: '2025-10-03T12:00:00Z' - BusinessCustomer: - title: Business Customer - allOf: - - $ref: '#/components/schemas/Customer' - - $ref: '#/components/schemas/BusinessCustomerFields' - - type: object - properties: - businessInfo: - $ref: '#/components/schemas/BusinessInfoResponse' - beneficialOwners: - type: array - items: - $ref: '#/components/schemas/BeneficialOwner' - CustomerOneOf: - oneOf: - - $ref: '#/components/schemas/IndividualCustomer' - - $ref: '#/components/schemas/BusinessCustomer' - discriminator: - propertyName: customerType - mapping: - INDIVIDUAL: '#/components/schemas/IndividualCustomer' - BUSINESS: '#/components/schemas/BusinessCustomer' - CustomerListResponse: + description: | + | Error Code | Description | + |------------|-------------| + | PAYREQ_REQUEST_FAILED | Payment request failed | + | COUNTERPARTY_PUBKEY_FETCH_ERROR | Error fetching counterparty public key | + | NO_COMPATIBLE_UMA_VERSION | No compatible UMA version | + | LNURLP_REQUEST_FAILED | LNURLP request failed | + | EMAIL_OTP_CREDENTIAL_SYNC_FAILED | Failed to update one or more tied EMAIL_OTP credentials | + enum: + - PAYREQ_REQUEST_FAILED + - COUNTERPARTY_PUBKEY_FETCH_ERROR + - NO_COMPATIBLE_UMA_VERSION + - LNURLP_REQUEST_FAILED + - EMAIL_OTP_CREDENTIAL_SYNC_FAILED + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + KycLinkCreateRequest: + type: object + description: Request body for generating a hosted KYC link for an existing customer. + properties: + redirectUri: + type: string + format: uri + description: URI the customer is redirected to after completing the hosted KYC flow. Must start with `https://` (or `http://` for local development). Embedded in the returned `kycUrl`. + example: https://app.example.com/onboarding/completed + KycProvider: + type: string + description: The KYC provider that will perform identity verification for the customer. Grid selects the provider based on the customer's region and platform configuration; the value is informational for platforms that want to integrate directly with the provider's SDK. + enum: + - SUMSUB + example: SUMSUB + KycLinkResponse: type: object + description: A hosted KYC link that the customer can complete to verify their identity. required: - - data - - hasMore + - kycUrl + - expiresAt + - provider properties: - data: - type: array - description: List of customers matching the filter criteria - items: - $ref: '#/components/schemas/CustomerOneOf' - hasMore: - type: boolean - description: Indicates if more results are available beyond this page - nextCursor: + kycUrl: 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) - Error405: + description: Hosted URL the customer should be sent to in order to complete verification. The URL is single-use and expires at `expiresAt`. To generate a new link (for example, after the previous one expires or is abandoned), call this endpoint again. + example: https://kyc.lightspark.com/onboard/abc123def456 + expiresAt: + type: string + format: date-time + description: Time at which the hosted link expires and can no longer be used. + example: '2027-01-15T14:32:00Z' + provider: + $ref: '#/components/schemas/KycProvider' + token: + type: string + description: Provider-specific token that can be used in place of the hosted URL — for example, to embed the provider's SDK directly in your application. Only returned for providers that support direct SDK integration. Whether to use the hosted URL or the embedded SDK is up to you; both flows result in the same `kycStatus` update on the customer. + example: _act-sbx-jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + ContactVerificationConfirmRequest: type: object + description: Request body for confirming an email or phone verification challenge. required: - - message - - status - code properties: - status: - type: integer - enum: - - 405 - description: HTTP status code code: type: string - description: | - | Error Code | Description | - |------------|-------------| - | METHOD_NOT_ALLOWED | The HTTP method is not supported for this endpoint | - enum: - - METHOD_NOT_ALLOWED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true - CustomerCreateRequest: + description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`. + example: '123456' + ScaFactor: + type: string + enum: + - SMS_OTP + - TOTP + - PASSKEY + description: | + A Strong Customer Authentication factor. + + | Factor | Description | + |--------|-------------| + | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | + | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | + | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | + ScaFactorView: type: object + description: An enrolled Strong Customer Authentication factor. required: - - customerType + - factor properties: - platformCustomerId: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The kind of enrolled factor. + credentialId: type: string - description: Platform-specific customer identifier. If not provided, one will be generated by the system. - example: 9f84e0c2a72c4fa - customerType: - $ref: '#/components/schemas/CustomerType' - region: + description: The per-credential id, populated only for `PASSKEY` factors (the id passed to delete a passkey). Omitted for `TOTP` and `SMS_OTP`, which have no per-credential id. + name: type: string - description: Country code (ISO 3166-1 alpha-2) representing the customer's regional identity. This determines the regulatory jurisdiction and KYC requirements for the customer. Required if the customer will use currencies with different KYC requirements across regions. A customer with accounts in multiple regions should be registered as separate customers. This field is immutable after creation. - example: US - currencies: + description: An optional human-readable label for this factor. + ScaFactorList: + type: object + description: The Strong Customer Authentication factors a customer has enrolled. + required: + - factors + properties: + factors: type: array + description: The customer's enrolled SCA factors. items: - type: string - description: List of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Required if the customer will use more than one sending currency, since the correct currencies cannot always be inferred. If not provided, currencies will be inferred from the customer's region. Some currency combinations may require separate customers — if so, the request will be rejected with details. - example: - - USD - - USDC - email: - type: string - format: email - description: Email address for the customer. - example: john.doe@example.com - phoneNumber: - type: string - pattern: ^\+[1-9]\d{1,14}$ - description: Phone number for the customer in strict E.164 format. - example: '+14155551234' - umaAddress: + $ref: '#/components/schemas/ScaFactorView' + TotpFactorEnrollRequest: + type: object + title: TOTP Factor Enroll Request + description: Start enrolling a time-based one-time-password (TOTP) authenticator factor. + required: + - type + properties: + type: type: string - description: Optional UMA address identifier. If not provided during customer creation, one will be generated by the system. If provided during customer update, the UMA address will be updated to the provided value. This is an optional identifier to route payments to the customer. This is an optional identifier to route payments to the customer. - example: $john.doe@uma.domain.com - IndividualCustomerCreateRequest: - title: Individual Customer Create Request - allOf: - - $ref: '#/components/schemas/CustomerCreateRequest' - - $ref: '#/components/schemas/IndividualCustomerFields' - - type: object - properties: - idType: - $ref: '#/components/schemas/IdentificationType' - identifier: - type: string - writeOnly: true - description: The individual's tax identification number. Required to onboard the individual as a US account holder. Only SSN and ITIN are currently accepted for an individual account holder; other identification types are rejected. Write-only — never returned in customer responses. - example: 123-45-6789 - BusinessInfo: + enum: + - TOTP + description: Discriminator selecting the TOTP factor. TOTP enrollment needs no other input at start. + PasskeyFactorEnrollRequest: type: object - description: Additional information required for business entities + title: Passkey Factor Enroll Request + description: Start enrolling a WebAuthn passkey factor. required: - - legalName - - taxId - - incorporatedOn + - type properties: - legalName: + type: type: string - description: Legal name of the business - example: Acme Corporation, Inc. - doingBusinessAs: + enum: + - PASSKEY + description: Discriminator selecting the passkey factor. Passkey enrollment needs no other input at start. + ScaFactorEnrollRequestOneOf: + oneOf: + - $ref: '#/components/schemas/TotpFactorEnrollRequest' + - $ref: '#/components/schemas/PasskeyFactorEnrollRequest' + discriminator: + propertyName: type + mapping: + TOTP: '#/components/schemas/TotpFactorEnrollRequest' + PASSKEY: '#/components/schemas/PasskeyFactorEnrollRequest' + description: Which SCA factor to begin enrolling, selected by `type`. `SMS_OTP` is not enrollable (it uses the customer's verified phone), so only `TOTP` and `PASSKEY` are valid here. + TotpEnrollmentStart: + type: object + description: The shared secret a customer's authenticator app needs to enroll a TOTP factor. Returned by `POST /sca/factors` for a `TOTP` request; the customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with the first code their app produces. + required: + - type + - secret + - secretBase32Encoded + - totpUri + properties: + type: type: string - description: Trade name or DBA name of the business, if different from the legal name - example: Acme - country: + enum: + - TOTP + description: Discriminator identifying this as the TOTP enrollment-start payload. + secret: type: string - description: Country of incorporation or registration (ISO 3166-1 alpha-2) - example: US - registrationNumber: + description: The raw TOTP shared secret. + secretBase32Encoded: type: string - description: Business registration number - example: '5523041' - incorporatedOn: + description: The Base32-encoded shared secret, suitable for manual entry into an authenticator app that does not scan QR codes. + totpUri: type: string - format: date - description: Date of incorporation in ISO 8601 format (YYYY-MM-DD) - example: '2018-03-14' - entityType: - $ref: '#/components/schemas/EntityType' - taxId: + description: The `otpauth://` provisioning URI (the QR-code payload) the customer's authenticator app scans to enroll the factor. + example: otpauth://totp/Grid:customer@example.com?secret=ABC123&issuer=Grid + PasskeyEnrollmentStart: + type: object + description: Opaque WebAuthn registration options relayed to the end user's device to enroll a passkey factor. Grid performs no crypto; pass `options` to the device's WebAuthn API to produce a credential, then submit that credential to the confirm endpoint unmodified. + required: + - type + - options + - allowedOrigins + - relyingPartyId + properties: + type: type: string - description: Tax identification number - example: 47-1234567 - countriesOfOperation: + enum: + - PASSKEY + description: Discriminator identifying this as the passkey enrollment-start payload. + options: + type: object + additionalProperties: true + description: Opaque WebAuthn `PublicKeyCredentialCreationOptions`. Pass to the device's WebAuthn registration API unmodified. + allowedOrigins: type: array + description: The origins the WebAuthn registration ceremony may run against. The origin the credential is produced against must be one of these and must be echoed back on the confirm call. items: type: string - description: List of countries where the business operates (ISO 3166-1 alpha-2) example: - - US - businessType: - $ref: '#/components/schemas/BusinessType' - purposeOfAccount: - $ref: '#/components/schemas/PurposeOfAccount' - sourceOfFunds: + - https://app.example.com + relyingPartyId: type: string - description: The primary source of funds for the business - example: Funds derived from customer payments for software services - expectedMonthlyTransactionCount: + description: The WebAuthn relying-party id the credential is bound to. + example: app.example.com + ScaFactorEnrollStartOneOf: + oneOf: + - $ref: '#/components/schemas/TotpEnrollmentStart' + - $ref: '#/components/schemas/PasskeyEnrollmentStart' + discriminator: + propertyName: type + mapping: + TOTP: '#/components/schemas/TotpEnrollmentStart' + PASSKEY: '#/components/schemas/PasskeyEnrollmentStart' + description: 'The factor-specific material needed to complete enrollment, keyed by `type`: a TOTP shared secret + provisioning URI, or the WebAuthn registration options for a passkey.' + TotpEnrollmentConfirmRequest: + type: object + description: The shared secret returned by the TOTP enrollment start, plus the first code the customer's authenticator app produces, submitted to confirm and finalize the TOTP factor. + required: + - type + - secret + - code + properties: + type: type: string enum: - - COUNT_UNDER_10 - - COUNT_10_TO_100 - - COUNT_100_TO_500 - - COUNT_500_TO_1000 - - COUNT_OVER_1000 - description: Expected number of transactions per month - example: COUNT_100_TO_500 - expectedMonthlyTransactionVolume: + - TOTP + description: Discriminator selecting the TOTP confirm variant. + secret: type: string - enum: - - VOLUME_UNDER_10K - - VOLUME_10K_TO_100K - - VOLUME_100K_TO_1M - - VOLUME_1M_TO_10M - - VOLUME_OVER_10M - description: Expected total transaction volume per month in USD equivalent - example: VOLUME_100K_TO_1M - expectedRecipientJurisdictions: - type: array - items: - type: string - description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2) - example: - - US - naicsCode: + description: The shared secret returned as `secret` by the TOTP enrollment start, threaded back to bind the confirmation to that enrollment. + code: type: string - pattern: ^\d{2,6}$ - description: NAICS code describing the nature of the business (2-6 digits) - example: '541511' - sourceOfFundsCategories: - type: array - items: - $ref: '#/components/schemas/SourceOfFundsCategory' - description: Structured source-of-funds categories for the business - example: - - OPERATING_REVENUE - sourceOfFundsOtherDescription: + description: The current time-based one-time code from the customer's authenticator app. In sandbox, the code is always `123456`. + example: '123456' + PasskeyEnrollmentConfirmRequest: + type: object + description: The WebAuthn credential a device produced for a passkey registration challenge, submitted to enroll the passkey factor. + required: + - type + - origin + - credential + properties: + type: type: string - minLength: 1 - maxLength: 500 - description: Description of the source of funds when OTHER is selected - example: Proceeds from a legal settlement - purposeOfAccountOtherDescription: + enum: + - PASSKEY + description: Discriminator selecting the passkey confirm variant. + origin: + type: string + description: The WebAuthn origin the `credential` was produced against (one of the enrollment start's `allowedOrigins`). + example: https://app.example.com + credential: + type: object + additionalProperties: true + description: Opaque WebAuthn credential the device produced from the enrollment start's `options`. + ScaFactorConfirmRequestOneOf: + oneOf: + - $ref: '#/components/schemas/TotpEnrollmentConfirmRequest' + - $ref: '#/components/schemas/PasskeyEnrollmentConfirmRequest' + discriminator: + propertyName: type + mapping: + TOTP: '#/components/schemas/TotpEnrollmentConfirmRequest' + PASSKEY: '#/components/schemas/PasskeyEnrollmentConfirmRequest' + description: 'The proof that finalizes enrollment, keyed by `type`: the TOTP shared secret + code, or the passkey `origin` + `credential`.' + TotpEnrollmentConfirmResponse: + type: object + description: The one-time recovery codes issued once a TOTP factor is enrolled. These are shown to the customer only once; store them somewhere safe to recover access if the authenticator device is lost. + required: + - type + - recoveryCodes + properties: + type: type: string - minLength: 1 - maxLength: 500 - description: Description of the account purpose when OTHER is selected - example: Escrow for equipment leases - expectedCounterpartyCountries: + enum: + - TOTP + description: Discriminator identifying this as the TOTP enrollment result. + recoveryCodes: type: array + description: The one-time recovery codes for this TOTP factor. items: type: string - description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) example: - - US - BusinessCustomerCreateRequest: - title: Business Customer Create Request - allOf: - - $ref: '#/components/schemas/CustomerCreateRequest' - - $ref: '#/components/schemas/BusinessCustomerFields' - - type: object - properties: - businessInfo: - $ref: '#/components/schemas/BusinessInfo' - CustomerCreateRequestOneOf: + - ABCD-EFGH-IJKL + - MNOP-QRST-UVWX + PasskeyEnrollmentConfirmResponse: + type: object + description: The enrolled passkey factor returned after a successful confirmation. + required: + - type + - factor + properties: + type: + type: string + enum: + - PASSKEY + description: Discriminator identifying this as the passkey enrollment result. + factor: + $ref: '#/components/schemas/ScaFactorView' + ScaFactorConfirmResponseOneOf: oneOf: - - $ref: '#/components/schemas/IndividualCustomerCreateRequest' - - $ref: '#/components/schemas/BusinessCustomerCreateRequest' + - $ref: '#/components/schemas/TotpEnrollmentConfirmResponse' + - $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse' discriminator: - propertyName: customerType + propertyName: type mapping: - INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest' - BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest' - Error409: + TOTP: '#/components/schemas/TotpEnrollmentConfirmResponse' + PASSKEY: '#/components/schemas/PasskeyEnrollmentConfirmResponse' + description: The enrollment result, keyed by `type`. + ScaLoginStartRequest: type: object + description: Selects which enrolled factor to start an SCA login with. The factor must already be enrolled (or, for `SMS_OTP`, the phone verified). required: - - message - - status - - code + - factor properties: - status: - type: integer - enum: - - 409 - description: HTTP status code + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor to authenticate with. + ScaLoginStart: + type: object + description: 'The factor-specific material a customer needs to complete an SCA login. Each factor surfaces only the fields it issues: `SMS_OTP` carries `challengeId` and `expiresAt`; `TOTP` carries neither (the customer reads the code from their authenticator app); `PASSKEY` carries the opaque WebAuthn `passkeyOptions` with `allowedOrigins` and `relyingPartyId`.' + required: + - factor + properties: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor this login was started for. + challengeId: + type: + - string + - 'null' + description: The challenge handle for an `SMS_OTP` login, threaded back on the complete call. Present only for `SMS_OTP`. + expiresAt: + type: + - string + - 'null' + format: date-time + description: Absolute UTC timestamp after which the `SMS_OTP` code expires. Present only for `SMS_OTP`. + example: '2025-10-03T12:05:00Z' + passkeyOptions: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion request options. Present only for `PASSKEY`; pass to the device's WebAuthn API to produce the assertion submitted on the complete call. + allowedOrigins: + type: + - array + - 'null' + items: + type: string + description: The origins the WebAuthn ceremony may run against. Present only for `PASSKEY`. + example: + - https://app.example.com + relyingPartyId: + type: + - string + - 'null' + description: The WebAuthn relying-party id. Present only for `PASSKEY`. + example: app.example.com + ScaLoginCompleteRequest: + type: object + description: Completes an SCA login by submitting the proof for the started factor. Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`), plus the `factor` being completed and, for `SMS_OTP`, the `challengeId` returned by the login start. + required: + - factor + anyOf: + - required: + - code + - required: + - passkeyAssertion + - origin + properties: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor being completed; must match the started login. + challengeId: + type: + - string + - 'null' + description: The challenge handle returned by the login start, required for `SMS_OTP` and omitted for other factors. code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL | Transaction is not pending platform approval | - | UMA_ADDRESS_EXISTS | UMA address already exists | - | EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential | - | EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued | - | CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform | - enum: - - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL - - UMA_ADDRESS_EXISTS - - EMAIL_OTP_EMAIL_ALREADY_EXISTS - - EMAIL_OTP_CREDENTIAL_SET_CHANGED - - CONFLICT - message: - type: string - description: Error message - details: - type: object - description: Additional error details + type: + - string + - 'null' + description: The one-time code the customer received by SMS, or read from their authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is always `123456`. + example: '123456' + passkeyAssertion: + type: + - object + - 'null' additionalProperties: true - Error404: + description: Opaque WebAuthn assertion produced by the device from the login start's `passkeyOptions`. Required when completing a `PASSKEY` login. + origin: + type: + - string + - 'null' + description: The WebAuthn origin the `passkeyAssertion` was produced against (one of the login start's `allowedOrigins`). Required alongside `passkeyAssertion`; omit it for the `code` path. + example: https://app.example.com + ScaLoginComplete: type: object + description: The status of a completed SCA login session. required: - - message - status - - code properties: status: - type: integer - enum: - - 404 - description: HTTP status code - code: type: string - description: | - | Error Code | Description | - |------------|-------------| - | TRANSACTION_NOT_FOUND | Transaction not found | - | INVITATION_NOT_FOUND | Invitation not found | - | USER_NOT_FOUND | Customer not found | - | QUOTE_NOT_FOUND | Quote not found | - | LOOKUP_REQUEST_NOT_FOUND | Lookup request not found | - | TOKEN_NOT_FOUND | Token not found | - | BULK_UPLOAD_JOB_NOT_FOUND | Bulk upload job not found | - | REFERENCE_NOT_FOUND | Reference not found | - | UMA_NOT_FOUND | The UMA address is well-formed but no receiver exists at the counterparty VASP | - | STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND | Stablecoin provider account link not found | - enum: - - TRANSACTION_NOT_FOUND - - INVITATION_NOT_FOUND - - USER_NOT_FOUND - - QUOTE_NOT_FOUND - - LOOKUP_REQUEST_NOT_FOUND - - TOKEN_NOT_FOUND - - BULK_UPLOAD_JOB_NOT_FOUND - - REFERENCE_NOT_FOUND - - UMA_NOT_FOUND - - STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true - Error410: + description: The status of the login session, passed through verbatim (Grid does not normalize it). A successful login reports `SUCCESS`; other values indicate the login did not complete and should be surfaced to the caller. + example: SUCCESS + Error423: type: object required: - message @@ -10793,97 +12217,117 @@ components: status: type: integer enum: - - 410 + - 423 description: HTTP status code code: type: string description: | | Error Code | Description | |------------|-------------| - | CUSTOMER_DELETED | Customer has been permanently deleted | + | ACCOUNT_LOCKED | The customer's login is temporarily locked (or suspended) after too many failed attempts. Inspect `details.lockedUntil` for when the customer may retry, and `details.failedAttempts` for the current count. | enum: - - CUSTOMER_DELETED + - ACCOUNT_LOCKED message: type: string description: Error message details: type: object - description: Additional error details + description: Lockout detail. `lockedUntil` is the UTC timestamp the customer may retry after (absent when suspended indefinitely); `failedAttempts` is the current cumulative failed-login count; `suspended` is true when the account is locked with no automatic expiry. + properties: + suspended: + type: boolean + lockedUntil: + type: + - string + - 'null' + format: date-time + failedAttempts: + type: integer additionalProperties: true - CustomerUpdateRequest: - title: Customer Update Request - description: Request body for `PATCH /customers/{customerId}`. When `email` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer email and every tied `EMAIL_OTP` credential through the endpoint's signed-retry flow. When `phoneNumber` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer phone number and every tied `SMS_OTP` credential through the same signed-retry flow. Update `email` and `phoneNumber` in separate PATCH calls. + RecordSecurityEventRequest: type: object + description: Records a client-side security event for the customer so Grid can maintain the customer's login-security state (SCA session revocation and failed-login lockout). required: - - customerType + - eventType properties: - customerType: - $ref: '#/components/schemas/CustomerType' - currencies: - type: array - items: - type: string - description: Updated list of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Replaces the existing list. Some currency combinations may require separate customers — if so, the request will be rejected with details. - example: - - USD - - EUR - - USDC - email: - type: string - format: email - description: Email address for the customer. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `EMAIL_OTP` credential across all tied Embedded Wallets. - example: john.doe@example.com - phoneNumber: + eventType: type: string - pattern: ^\+[1-9]\d{1,14}$ - description: Phone number for the customer in strict E.164 format. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `SMS_OTP` credential across all tied Embedded Wallets. Send phone number and email updates as separate PATCH calls. - example: '+14155551234' - umaAddress: - type: string - description: Optional UMA address identifier. If provided, the customer's UMA address will be updated. This is an optional identifier to route payments to the customer. - example: $john.doe@uma.domain.com - IndividualCustomerUpdateRequest: - title: Individual Customer Update Request - allOf: - - $ref: '#/components/schemas/CustomerUpdateRequest' - - $ref: '#/components/schemas/IndividualCustomerFields' - BusinessCustomerUpdateRequest: - title: Business Customer Update Request - allOf: - - $ref: '#/components/schemas/CustomerUpdateRequest' - - $ref: '#/components/schemas/BusinessCustomerFields' - CustomerUpdateRequestOneOf: - oneOf: - - $ref: '#/components/schemas/IndividualCustomerUpdateRequest' - - $ref: '#/components/schemas/BusinessCustomerUpdateRequest' - discriminator: - propertyName: customerType - mapping: - INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' - BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' - SignedRequestChallenge: - title: Signed Request Challenge + enum: + - RESET_PASSWORD_COMPLETED + - FAILED_LOGIN_ATTEMPT + description: | + The security event to record: + + | Value | Effect | + |-------|--------| + | `RESET_PASSWORD_COMPLETED` | Revokes every active SCA session for the customer and clears the failed-login counter. | + | `FAILED_LOGIN_ATTEMPT` | Increments the cumulative failed-login counter and escalates the lockout at each milestone: 5 attempts → 15 minutes, 6 → 30 minutes, 7 → 1 hour, 8 → 24 hours, 9 or more → suspension. | + example: FAILED_LOGIN_ATTEMPT + RecordSecurityEventResponse: type: object + description: The customer's login-security state after recording the event, so the integrator can surface a lockout to the end user. required: - - payloadToSign - - requestId - - expiresAt - description: Common base for two-step signed-retry challenge responses on Embedded Wallet endpoints (credential registration or revocation, session refresh or revocation, wallet export, customer email updates, and similar). Holds the signing fields shared across every challenge shape; each variant composes this base via `allOf` and adds its own resource `id` (and `type`, when applicable) with variant-specific description and example. + - eventType + - suspended + - failedAttempts properties: - payloadToSign: + eventType: type: string - description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request. - example: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_EXAMPLE"}' - requestId: + enum: + - RESET_PASSWORD_COMPLETED + - FAILED_LOGIN_ATTEMPT + description: The event that was recorded. + example: FAILED_LOGIN_ATTEMPT + suspended: + type: boolean + description: Whether the customer's login is currently suspended (locked with no automatic expiry). A suspended customer must go through a password reset (`RESET_PASSWORD_COMPLETED`) to clear the lockout. + lockedUntil: + type: + - string + - 'null' + format: date-time + description: When the customer may attempt to log in again, if temporarily locked. Null when not locked, or when `suspended` is true (no automatic expiry). + example: '2025-10-03T12:15:00Z' + failedAttempts: + type: integer + description: The customer's current cumulative failed-login count. + example: 5 + TwoFactorResetStartRequest: + type: object + description: Selects which enrolled factor to reset via the liveness-gated recovery flow. + required: + - factor + properties: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The enrolled factor to reset. + TwoFactorResetStart: + type: object + description: The reset handle plus the opaque liveness handles a caller relays to the end-user device to complete the liveness check. `resetId` threads the ceremony together (status and complete reference it). `livenessAccessToken` and `verificationLink` are omitted when they are not returned. + required: + - resetId + properties: + resetId: type: string - description: Grid-issued `Request:` identifier for this pending request. Echo this value exactly in the `Request-Id` header on the signed retry so the server can correlate the retry with the issued challenge. - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + description: Identifier for this reset; pass it to the status and complete endpoints. + livenessAccessToken: + type: + - string + - 'null' + description: Access token for the embedded liveness/verification SDK, bound to this reset. Omitted when one is not returned. + verificationLink: + type: + - string + - 'null' + description: Hosted identity-verification page URL for completing liveness. Omitted when one is not returned. expiresAt: - type: string + type: + - string + - 'null' format: date-time - description: Timestamp after which this challenge is no longer valid. The signed retry must be submitted before this time. - example: '2026-04-08T15:35:00Z' - Error424: + description: Absolute UTC timestamp at the end of the reset window. Omitted when one is not returned. + example: '2025-10-03T12:30:00Z' + Error429: type: object required: - message @@ -10893,24 +12337,16 @@ components: status: type: integer enum: - - 424 + - 429 description: HTTP status code code: type: string description: | | Error Code | Description | |------------|-------------| - | PAYREQ_REQUEST_FAILED | Payment request failed | - | COUNTERPARTY_PUBKEY_FETCH_ERROR | Error fetching counterparty public key | - | NO_COMPATIBLE_UMA_VERSION | No compatible UMA version | - | LNURLP_REQUEST_FAILED | LNURLP request failed | - | EMAIL_OTP_CREDENTIAL_SYNC_FAILED | Failed to update one or more tied EMAIL_OTP credentials | + | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header | enum: - - PAYREQ_REQUEST_FAILED - - COUNTERPARTY_PUBKEY_FETCH_ERROR - - NO_COMPATIBLE_UMA_VERSION - - LNURLP_REQUEST_FAILED - - EMAIL_OTP_CREDENTIAL_SYNC_FAILED + - RATE_LIMITED message: type: string description: Error message @@ -10918,54 +12354,175 @@ components: type: object description: Additional error details additionalProperties: true - KycLinkCreateRequest: + TwoFactorResetStatus: type: object - description: Request body for generating a hosted KYC link for an existing customer. + description: The status of an in-progress 2FA reset, polled until it reaches a terminal value. + required: + - status + - factor + - expiresAt properties: - redirectUri: + status: type: string - format: uri - description: URI the customer is redirected to after completing the hosted KYC flow. Must start with `https://` (or `http://` for local development). Embedded in the returned `kycUrl`. - example: https://app.example.com/onboarding/completed - KycProvider: - type: string - description: The KYC provider that will perform identity verification for the customer. Grid selects the provider based on the customer's region and platform configuration; the value is informational for platforms that want to integrate directly with the provider's SDK. - enum: - - SUMSUB - example: SUMSUB - KycLinkResponse: + enum: + - INITIATED + - PENDING_REVIEW + - LIVENESS_PASSED + - COMPLETED + - REJECTED + - EXPIRED + description: |- + The reset status. + | Value | Terminal | Meaning | |-------|----------|---------| | `INITIATED` | no | Reset started; liveness not yet submitted. Keep polling. | | `PENDING_REVIEW` | no | Liveness submitted; under review. Keep polling. | | `LIVENESS_PASSED` | no | Liveness passed; call the complete endpoint to finish the reset. | | `COMPLETED` | **yes** | Reset finished; the lost factor is cleared and re-enrollment can begin. | | `REJECTED` | **yes** | Liveness failed. Stop polling; start a new reset. | | `EXPIRED` | **yes** | The reset window closed before it completed. Stop polling; start a new reset. | + Stop polling on any terminal value. + example: INITIATED + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor being reset. + enrollmentStatus: + type: + - string + - 'null' + enum: + - PENDING + - COMPLETED + - null + description: The enrollment status of the replacement factor. `PENDING` until the customer finishes re-enrolling, then `COMPLETED`. Null for an `SMS_OTP` reset, where completing the reset enrolls the new number directly with no separate enrollment step. + example: PENDING + expiresAt: + type: string + format: date-time + description: When the reset window closes. Poll no longer than this. + example: '2025-10-03T12:15:00Z' + completedAt: + type: + - string + - 'null' + format: date-time + description: When the reset completed; null until then. + example: null + TwoFactorResetCompleteRequest: type: object - description: A hosted KYC link that the customer can complete to verify their identity. + description: Optional body for completing a 2FA reset. Only needed when resetting the `SMS_OTP` factor to a new phone number; omit the body entirely otherwise. + properties: + mobile: + type: object + description: For an `SMS_OTP` reset, the new mobile number to enroll as the customer completes the reset. Required for an `SMS_OTP` reset; ignored for `TOTP` / `PASSKEY` resets. + required: + - countryCode + - number + properties: + countryCode: + type: string + description: The country dialing code, including the leading `+`. + example: '+1' + number: + type: string + description: The national subscriber number, without the country code. + example: '4155550123' + ScaChallenge: + type: object + description: |- + A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. + + When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`. + + **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. required: - - kycUrl + - id - expiresAt - - provider + - factor + - availableFactors properties: - kycUrl: + id: type: string - description: Hosted URL the customer should be sent to in order to complete verification. The URL is single-use and expires at `expiresAt`. To generate a new link (for example, after the previous one expires or is abandoned), call this endpoint again. - example: https://kyc.lightspark.com/onboard/abc123def456 + description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). + example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 expiresAt: type: string format: date-time - description: Time at which the hosted link expires and can no longer be used. - example: '2027-01-15T14:32:00Z' - provider: - $ref: '#/components/schemas/KycProvider' - token: - type: string - description: Provider-specific token that can be used in place of the hosted URL — for example, to embed the provider's SDK directly in your application. Only returned for providers that support direct SDK integration. Whether to use the hosted URL or the embedded SDK is up to you; both flows result in the same `kycStatus` update on the customer. - example: _act-sbx-jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - ContactVerificationConfirmRequest: + description: Absolute UTC timestamp after which this challenge can no longer be authorized. + example: '2025-10-03T12:05:00Z' + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor this challenge was issued for. Defaults to `SMS_OTP`. + availableFactors: + type: array + description: The factors the customer may use to satisfy this challenge. + items: + $ref: '#/components/schemas/ScaFactor' + example: + - SMS_OTP + purpose: + type: + - string + - 'null' + description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). + example: PAYOUT + passkeyAssertionOptions: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. + passkeyAllowedOrigins: + type: + - array + - 'null' + items: + type: string + description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. + example: + - https://app.example.com + BeneficiaryTrustStart: type: object - description: Request body for confirming an email or phone verification challenge. - required: - - code + description: The SCA challenge (if any) a caller authorizes to finish trusting (or untrusting) a beneficiary. The beneficiary is identified by its `externalAccountId`, so the confirm call needs no separate handle. `scaChallenge` is omitted when no challenge is issued; the caller then confirms without a `challengeId`. + properties: + scaChallenge: + $ref: '#/components/schemas/ScaChallenge' + description: The SCA challenge to satisfy on the confirm call. Omitted when no challenge is issued. + BeneficiaryTrustConfirmRequest: + type: object + description: Confirms trusting or untrusting a beneficiary by submitting the SCA proof. Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`) and, when the start issued one, the `challengeId`. The beneficiary is identified by the `externalAccountId` in the path — no separate handle is needed. + anyOf: + - required: + - code + - required: + - passkeyAssertion + - origin properties: + challengeId: + type: + - string + - 'null' + description: The challenge handle from the trust start's `scaChallenge`, when one was issued. Omit when the start returned no challenge. code: - type: string - description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`. + type: + - string + - 'null' + description: The one-time code the customer received by SMS, or read from their authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is always `123456`. example: '123456' + passkeyAssertion: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion produced by the device from the challenge's assertion options. Required when satisfying a `PASSKEY` challenge. + origin: + type: + - string + - 'null' + description: The WebAuthn origin the `passkeyAssertion` was produced against. Required alongside `passkeyAssertion`; omit it for the `code` path. + example: https://app.example.com + BeneficiaryTrustConfirm: + type: object + description: The result of a confirm-trust / confirm-untrust call. `trusted` is `true` after a successful trust and `false` after a successful untrust. + required: + - trusted + properties: + trusted: + type: boolean + description: Whether the beneficiary is now trusted. `true` after a successful trust, `false` after a successful untrust. InternalAccountType: title: Internal Account Type type: string @@ -18202,20 +19759,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' - ScaFactor: - type: string - enum: - - SMS_OTP - - TOTP - - PASSKEY - description: | - A Strong Customer Authentication factor. - - | Factor | Description | - |--------|-------------| - | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | - | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | - | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | TransferOutRequest: type: object required: @@ -18238,9 +19781,6 @@ components: maxLength: 80 description: 'Free-form information about the payment that travels with it 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.' example: '12345' - scaFactor: - $ref: '#/components/schemas/ScaFactor' - description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. CurrencyPreference: type: object required: @@ -18475,60 +20015,6 @@ components: mapping: ACCOUNT: '#/components/schemas/AccountDestination' UMA_ADDRESS: '#/components/schemas/UmaAddressDestination' - ScaChallenge: - type: object - description: |- - A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. - - When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`. - - **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. - required: - - id - - expiresAt - - factor - - availableFactors - properties: - id: - type: string - description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). - example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 - expiresAt: - type: string - format: date-time - description: Absolute UTC timestamp after which this challenge can no longer be authorized. - example: '2025-10-03T12:05:00Z' - factor: - $ref: '#/components/schemas/ScaFactor' - description: The factor this challenge was issued for. Defaults to `SMS_OTP`. - availableFactors: - type: array - description: The factors the customer may use to satisfy this challenge. - items: - $ref: '#/components/schemas/ScaFactor' - example: - - SMS_OTP - purpose: - type: - - string - - 'null' - description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). - example: PAYOUT - passkeyAssertionOptions: - type: - - object - - 'null' - additionalProperties: true - description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. - passkeyAllowedOrigins: - type: - - array - - 'null' - items: - type: string - description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. - example: - - https://app.example.com Quote: type: object required: @@ -18753,33 +20239,6 @@ components: - 'null' description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`. example: https://app.example.com - Error429: - type: object - required: - - message - - status - - code - properties: - status: - type: integer - enum: - - 429 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header | - enum: - - RATE_LIMITED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true TransactionListResponse: type: object required: diff --git a/openapi.yaml b/openapi.yaml index 1e57d7e52..40e821c1a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1127,180 +1127,97 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/internal-accounts: + /sca/factors: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer whose factors are listed or enrolled. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 get: - summary: List Customer internal accounts + summary: List enrolled SCA factors description: | - Retrieve a list of internal accounts with optional filtering parameters. Returns all - internal accounts that match the specified filters. If no filters are provided, returns all internal accounts - (paginated). + List the Strong Customer Authentication factors the customer has enrolled. - Internal accounts are created automatically when a customer is created based on the platform configuration. - operationId: listCustomerInternalAccounts + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: listScaFactors tags: - - Internal Accounts + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: currency - in: query - description: Filter by currency code - required: false - schema: - type: string - - name: customerId - in: query - description: Filter by internal accounts associated with a specific customer - required: false - schema: - 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. - required: false - schema: - $ref: '#/components/schemas/InternalAccountType' - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string responses: '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/InternalAccountListResponse' - '400': - description: Bad request - Invalid parameters + description: The customer's enrolled SCA factors. content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/ScaFactorList' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /platform/internal-accounts: - get: - summary: List platform internal accounts - description: | - Retrieve a list of all internal accounts that belong to the platform, as opposed to an individual customer. - - These accounts are created automatically when the platform is configured for each supported currency. They can be used for things like distributing bitcoin rewards to customers, or for other platform-wide purposes. - operationId: listPlatformInternalAccounts - tags: - - Internal Accounts - security: - - BasicAuth: [] - parameters: - - name: currency - in: query - description: Filter by currency code - required: false - schema: - 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. - required: false - schema: - $ref: '#/components/schemas/InternalAccountType' - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/PlatformInternalAccountListResponse' - '400': - description: Bad request - Invalid parameters + '404': + description: Customer not found content: application/json: schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. content: application/json: schema: - $ref: '#/components/schemas/Error401' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/external-accounts: - get: - summary: List Customer external accounts + post: + summary: Start SCA factor enrollment description: | - Retrieve a list of external accounts with optional filtering parameters. Returns all - external accounts that match the specified filters. If no filters are provided, returns all external accounts - (paginated). + Begin enrolling an SCA factor for the customer. Enrollment covers the + explicit, opt-in factors a customer chooses to add — the request body's + `type` selects `TOTP` or `PASSKEY`. Returns the factor-specific material + needed to finish via `POST /sca/factors/confirm`. - External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform. - operationId: listCustomerExternalAccounts + `SMS_OTP` is implicit and is not enrolled through this endpoint. Every + customer in an SCA-regulated region has a verified phone number from + customer creation (via the Contact Verification flows — + `POST /customers/{customerId}/verify-phone` and `.../verify-phone/confirm`), + so SMS is always available as a factor with no extra setup and appears + among the customer's enrolled factors in `GET /sca/factors`. + + A customer may have **only one passkey**. Starting a passkey enrollment when + one is already enrolled returns `409` (`PASSKEY_ALREADY_ENROLLED`) — delete it + via `DELETE /sca/factors/{credentialId}` first. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startScaFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: currency - in: query - description: Filter by currency code - required: false - schema: - type: string - - name: customerId - in: query - description: Filter by external accounts associated with a specific customer - required: false - schema: - type: string - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaFactorEnrollRequestOneOf' responses: '200': - description: Successful operation + description: Enrollment started; the factor-specific completion material is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' + $ref: '#/components/schemas/ScaFactorEnrollStartOneOf' '400': - description: Bad request - Invalid parameters + description: Invalid request content: application/json: schema: @@ -1311,18 +1228,52 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer (`CONFLICT`), or a passkey enrollment was requested while one is already enrolled (`PASSKEY_ALREADY_ENROLLED`) — only one passkey per customer is supported. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /sca/factors/confirm: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer confirming a factor enrollment. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Add a new external account - description: Register a new external bank account for a customer. - operationId: createCustomerExternalAccount + summary: Confirm SCA factor enrollment + description: | + Finalize the factor enrollment started by `POST /sca/factors`. The request + body is discriminated by `type`: for `TOTP`, submit the shared `secret` from + the start call plus the first `code`; for `PASSKEY`, submit the WebAuthn + `credential` the device produced plus the `origin` it was produced against. + The threaded secret/credential binds the confirmation to its enrollment, so + no separate id is needed. + + A TOTP confirmation returns one-time recovery codes (shown once); a passkey + confirmation returns the enrolled factor. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the TOTP code is always `123456`. + operationId: confirmScaFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1330,48 +1281,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountCreateRequest' - examples: - usBankAccount: - summary: Create external US bank account - value: - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - currency: USD - accountInfo: - accountType: USD_ACCOUNT - accountNumber: '12345678901' - routingNumber: '123456789' - bankAccountType: CHECKING - bankName: Chase Bank - platformAccountId: ext_acc_123456 - beneficiary: - beneficiaryType: INDIVIDUAL - fullName: John Doe - birthDate: '1990-01-15' - nationality: US - address: - line1: 123 Main Street - city: San Francisco - state: CA - postalCode: '94105' - country: US - sparkWallet: - summary: Create external Spark wallet - value: - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - currency: BTC - accountInfo: - accountType: SPARK_WALLET - address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu + $ref: '#/components/schemas/ScaFactorConfirmRequestOneOf' responses: - '201': - description: External account created successfully + '200': + description: Factor enrolled; the factor-specific result is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/ScaFactorConfirmResponseOneOf' '400': - description: Bad request + description: Invalid or incorrect confirmation proof content: application/json: schema: @@ -1382,8 +1301,14 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '409': - description: Conflict - External account already exists + description: SCA is not required for this customer. content: application/json: schema: @@ -1394,29 +1319,36 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/external-accounts/{externalAccountId}: + /sca/factors/{credentialId}: parameters: - - name: externalAccountId + - name: customerId + in: query + description: The unique identifier of the customer whose factor is being deleted. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: credentialId in: path - description: System-generated unique external account identifier + description: The credential id of the enrolled factor to delete (from the factor's `credentialId`). required: true schema: type: string - get: - summary: Get customer external account by ID - description: Retrieve a customer external account by its system-generated ID - operationId: getCustomerExternalAccountById + delete: + summary: Delete an enrolled SCA factor + description: | + Delete an enrolled SCA factor by its credential id. Today only `PASSKEY` + factors carry a `credentialId` and are deletable this way. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: deleteScaFactor tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ExternalAccount' + '204': + description: Factor deleted; no content is returned. '401': description: Unauthorized content: @@ -1424,89 +1356,64 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer or factor not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '500': - description: Internal service error + '409': + description: SCA is not required for this customer. content: application/json: schema: - $ref: '#/components/schemas/Error500' - delete: - summary: Delete customer external account by ID - description: Delete a customer external account by its system-generated ID - operationId: deleteCustomerExternalAccountById - tags: - - External Accounts - security: - - BasicAuth: [] - responses: - '204': - description: External account deleted successfully - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: External account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts: - get: - summary: List platform external accounts + /sca/login/start: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer starting an SCA login. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Start an SCA login description: | - Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer. + Begin an SCA login for the customer with the chosen factor, opening the + end-user SCA session (an exemption gating read / account access beyond the + per-transaction window). Returns factor-specific material: `SMS_OTP` + dispatches a code and returns a `challengeId` + `expiresAt`; `TOTP` returns + only the factor (the customer reads the code from their app); `PASSKEY` + returns WebAuthn `passkeyOptions`. Complete with + `POST /sca/login/complete`. - These accounts are used for platform-wide operations such as receiving funds from external sources or managing platform-level payment destinations. - operationId: listPlatformExternalAccounts + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startScaLogin tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: currency - in: query - description: Filter by currency code - required: false - schema: - type: string - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaLoginStartRequest' responses: '200': - description: Successful operation + description: SCA login started; factor-specific material is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' + $ref: '#/components/schemas/ScaLoginStart' '400': - description: Bad request - Invalid parameters + description: Invalid or unknown factor content: application/json: schema: @@ -1517,18 +1424,47 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /sca/login/complete: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer completing an SCA login. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Add a new platform external account - description: Register a new external bank account for the platform. - operationId: createPlatformExternalAccount + summary: Complete an SCA login + description: | + Finalize an SCA login by submitting the proof for the started factor + (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for + `PASSKEY`), echoing the `challengeId` for `SMS_OTP`. Returns the + reported session status. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: completeScaLogin tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1536,46 +1472,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PlatformExternalAccountCreateRequest' - examples: - usBankAccount: - summary: Create external US bank account - value: - currency: USD - accountInfo: - accountType: USD_ACCOUNT - accountNumber: '12345678901' - routingNumber: '123456789' - bankAccountType: CHECKING - bankName: Chase Bank - platformAccountId: ext_acc_123456 - beneficiary: - beneficiaryType: INDIVIDUAL - fullName: John Doe - birthDate: '1990-01-15' - nationality: US - address: - line1: 123 Main Street - city: San Francisco - state: CA - postalCode: '94105' - country: US - sparkWallet: - summary: Create external Spark wallet - value: - currency: BTC - accountInfo: - accountType: SPARK_WALLET - address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu + $ref: '#/components/schemas/ScaLoginCompleteRequest' responses: - '201': - description: External account created successfully + '200': + description: SCA login completed; the session status is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/ScaLoginComplete' '400': - description: Bad request + description: Invalid or expired proof content: application/json: schema: @@ -1586,41 +1492,70 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '409': - description: Conflict - External account already exists + description: SCA is not required for this customer. content: application/json: schema: $ref: '#/components/schemas/Error409' + '423': + description: The customer's login is locked (or suspended) after too many failed attempts. `details.lockedUntil` says when they may retry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error423' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts/{externalAccountId}: + /sca/record-event: parameters: - - name: externalAccountId - in: path - description: System-generated unique external account identifier + - name: customerId + in: query + description: The unique identifier of the customer the security event is recorded for. required: true schema: type: string - get: - summary: Get platform external account by ID - description: Retrieve a platform external account by its system-generated ID - operationId: getPlatformExternalAccountById + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Record a security event + description: | + Record a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view), to + feed adaptive-authentication signals. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: recordSecurityEvent tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RecordSecurityEventRequest' responses: '200': - description: Successful operation + description: Event recorded; the customer's resulting login-security state is returned (including any lockout). content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/RecordSecurityEventResponse' + '400': + description: Invalid event type + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1628,54 +1563,52 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - delete: - summary: Delete platform external account by ID - description: Delete a platform external account by its system-generated ID - operationId: deletePlatformExternalAccountById - tags: - - External Accounts - security: - - BasicAuth: [] - responses: - '204': - description: External account deleted successfully - '401': - description: Unauthorized + '409': + description: SCA is not required for this customer. content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: External account not found + $ref: '#/components/schemas/Error409' + '423': + description: The customer's login is locked (or suspended) after too many failed attempts. `details.lockedUntil` says when they may retry. content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error423' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /beneficial-owners: + /sca/factors/reset: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer resetting a factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Create a beneficial owner + summary: Start a 2FA reset description: | - Add a beneficial owner, director, or company officer to a business customer. The beneficial owner will go through KYC verification automatically. - operationId: createBeneficialOwner + Begin recovering a lost enrolled factor via a liveness-gated, poll-based + flow. Opens the liveness check and returns a `resetId` plus the + opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end + user completes it with. Poll + `GET /sca/factors/reset/{resetId}` until liveness + passes, then call the complete endpoint. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startTwoFactorReset tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1683,16 +1616,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwnerCreateRequest' + $ref: '#/components/schemas/TwoFactorResetStartRequest' responses: '201': - description: Beneficial owner created successfully + description: Reset initiated; the reset handle and liveness material are returned. content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwner' + $ref: '#/components/schemas/TwoFactorResetStart' '400': - description: Bad request - Invalid parameters + description: Invalid or unknown factor content: application/json: schema: @@ -1709,139 +1642,122 @@ paths: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '429': + description: Too many reset attempts. Reset initiation is rate-limited to 5 per 24 hours per customer; retry after the window indicated by `Retry-After`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /sca/factors/reset/{resetId}: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer whose reset status is polled. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: resetId + in: path + description: The reset handle returned by the start call. + required: true + schema: + type: string get: - summary: List beneficial owners + summary: Get 2FA reset status description: | - Retrieve a list of beneficial owners for a business customer. - operationId: listBeneficialOwners + Poll the status of an in-progress 2FA reset until it reaches the liveness-passed value, after which the reset can be completed. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: getTwoFactorResetStatus tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: customerId - in: query - description: The business customer ID - required: true - schema: - type: string - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string responses: '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/BeneficialOwnerListResponse' - '400': - description: Bad request - Invalid parameters + description: The current reset status. content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/TwoFactorResetStatus' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /beneficial-owners/{beneficialOwnerId}: - get: - summary: Get a beneficial owner - description: Retrieve details of a specific beneficial owner by ID. - operationId: getBeneficialOwner - tags: - - KYC/KYB Verifications - security: - - BasicAuth: [] - parameters: - - name: beneficialOwnerId - in: path - description: Beneficial owner ID - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/BeneficialOwner' - '401': - description: Unauthorized + '404': + description: Customer or reset not found content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Beneficial owner not found + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - patch: - summary: Update a beneficial owner - description: Update details of a specific beneficial owner. Only provided fields are updated. - operationId: updateBeneficialOwner + /sca/factors/reset/{resetId}/complete: + parameters: + - name: customerId + in: query + description: The unique identifier of the customer completing the reset. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: resetId + in: path + description: The reset handle returned by the start call. + required: true + schema: + type: string + post: + summary: Complete a 2FA reset + description: | + Complete a 2FA reset once liveness has passed, clearing the lost factor so + the customer can re-enroll. + + For an `SMS_OTP` reset, supply the new `mobile` number in the body — completing + the reset enrolls it as the customer's number. Other factors need no body. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: completeTwoFactorReset tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: beneficialOwnerId - in: path - description: Beneficial owner ID - required: true - schema: - type: string requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwnerUpdateRequest' + $ref: '#/components/schemas/TwoFactorResetCompleteRequest' responses: - '200': - description: Beneficial owner updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/BeneficialOwner' + '204': + description: Reset completed; no content is returned. '400': - description: Bad request - Invalid parameters + description: Reset not ready (liveness not yet passed) content: application/json: schema: @@ -1853,40 +1769,54 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Beneficial owner not found + description: Customer or reset not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /documents: + /customers/external-accounts/{externalAccountId}/trust: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being trusted. + required: true + schema: + type: string post: - summary: Upload a document + summary: Start trusting a beneficiary description: | - Upload a verification document for a customer or beneficial owner. The request must use multipart/form-data with the file in the `file` field and metadata in the remaining fields. + Begin trusting (whitelisting) an external account so future sends to it can + skip the per-transaction SCA ceremony. Returns the `scaChallenge` to satisfy + (when one is issued). Complete with + `POST /customers/external-accounts/{externalAccountId}/trust/confirm`. - Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB. - operationId: uploadDocument + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startBeneficiaryTrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] - requestBody: - $ref: '#/components/requestBodies/DocumentUploadRequestBody' responses: - '201': - description: Document uploaded successfully + '200': + description: Beneficiary trust started; the SCA challenge (if any) is returned. content: application/json: schema: - $ref: '#/components/schemas/Document' + $ref: '#/components/schemas/BeneficiaryTrustStart' '400': - description: Bad request - Invalid file type, size, or parameters + description: Invalid request content: application/json: schema: @@ -1898,57 +1828,62 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Document holder not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - get: - summary: List documents + /customers/external-accounts/{externalAccountId}/trust/confirm: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being trusted. + required: true + schema: + type: string + post: + summary: Confirm trusting a beneficiary description: | - Retrieve a list of documents with optional filtering by document holder. - operationId: listDocuments + Finalize trusting a beneficiary (identified by the `externalAccountId` in the + path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when + one was issued. Returns `trusted: true`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryTrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: documentHolder - in: query - description: Filter by document holder ID (Customer or BeneficialOwner) - required: false - schema: - type: string - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest' responses: '200': - description: Successful operation + description: Beneficiary trusted. content: application/json: schema: - $ref: '#/components/schemas/DocumentListResponse' + $ref: '#/components/schemas/BeneficiaryTrustConfirm' '400': - description: Bad request - Invalid parameters + description: Invalid or expired proof content: application/json: schema: @@ -1959,35 +1894,60 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /documents/{documentId}: - get: - summary: Get a document by ID - description: Retrieve details and metadata of a specific document by ID. - operationId: getDocument + /customers/external-accounts/{externalAccountId}/untrust: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being untrusted. + required: true + schema: + type: string + post: + summary: Start untrusting a beneficiary + description: | + Begin untrusting (removing the trusted mark from) an external account, so + future sends to it are dynamically linked and require the per-transaction SCA + ceremony again. Returns the `scaChallenge` to satisfy (when one is issued). + Complete with + `POST /customers/external-accounts/{externalAccountId}/untrust/confirm`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startBeneficiaryUntrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: documentId - in: path - description: Document ID - required: true - schema: - type: string responses: '200': - description: Successful operation + description: Beneficiary untrust started; the SCA challenge (if any) is returned. content: application/json: schema: - $ref: '#/components/schemas/Document' + $ref: '#/components/schemas/BeneficiaryTrustStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1995,44 +1955,62 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Document not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - put: - summary: Replace a document + /customers/external-accounts/{externalAccountId}/untrust/confirm: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being untrusted. + required: true + schema: + type: string + post: + summary: Confirm untrusting a beneficiary description: | - Replace an existing document with a new file and/or updated metadata. This is useful when a document was rejected and needs to be re-uploaded. The request must use multipart/form-data. - operationId: replaceDocument + Finalize untrusting a beneficiary (identified by the `externalAccountId` in + the path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when + one was issued. Returns `trusted: false`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryUntrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: documentId - in: path - description: Document ID - required: true - schema: - type: string requestBody: - $ref: '#/components/requestBodies/DocumentReplaceRequestBody' + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest' responses: '200': - description: Document replaced successfully + description: Beneficiary untrusted. content: application/json: schema: - $ref: '#/components/schemas/Document' + $ref: '#/components/schemas/BeneficiaryTrustConfirm' '400': - description: Bad request - Invalid file type, size, or parameters + description: Invalid or expired proof content: application/json: schema: @@ -2044,158 +2022,128 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Document not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - delete: - summary: Delete a document + /customers/internal-accounts: + get: + summary: List Customer internal accounts description: | - Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable. - operationId: deleteDocument + Retrieve a list of internal accounts with optional filtering parameters. Returns all + internal accounts that match the specified filters. If no filters are provided, returns all internal accounts + (paginated). + + Internal accounts are created automatically when a customer is created based on the platform configuration. + operationId: listCustomerInternalAccounts tags: - - Documents + - Internal Accounts security: - BasicAuth: [] parameters: - - name: documentId - in: path - description: Document ID - required: true + - name: currency + in: query + description: Filter by currency code + required: false + schema: + type: string + - name: customerId + in: query + description: Filter by internal accounts associated with a specific customer + required: false + schema: + 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. + required: false + schema: + $ref: '#/components/schemas/InternalAccountType' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false schema: type: string responses: - '204': - description: Document deleted successfully - '401': - description: Unauthorized + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Document not found + $ref: '#/components/schemas/InternalAccountListResponse' + '400': + description: Bad request - Invalid parameters content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Document cannot be deleted (already submitted for verification) + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/Error401' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /verifications: - post: - summary: Submit customer for verification + /platform/internal-accounts: + get: + summary: List platform internal accounts description: | - Trigger KYC (individual) or KYB (business) verification for a customer. - The response indicates whether all required information has been provided. - If data is missing, the `errors` array describes exactly what needs to be - supplied before verification can proceed. - - Call this endpoint again after resolving errors to re-submit. - - ### What to collect for KYB - - Before submitting a `BUSINESS` customer, collect the following via - `POST /customers`, `POST /beneficial-owners`, and `POST /documents`: - - **Business identifying information** - - Entity full legal name - - Doing Business As (DBA) name, if applicable - - Physical address — principal place of business - - Countries of operation - - Identification number — U.S. taxpayer identification number, or, for a - foreign business without one, alternative government-issued documentation - certifying the existence of the business - - **Ownership and control structure** — collected for **one control person** - (an individual with significant responsibility to control, manage, or - direct the legal entity) **and all beneficial owners** (every individual - who owns 25% or more, directly or indirectly). For each, provide: - - Full name - - Date of birth - - Address - - Identification number: - - U.S. persons — SSN or ITIN - - Non-U.S. persons — one or more of: ITIN, passport (with country of - issuance), alien identification card, or another government-issued - photo ID evidencing nationality or residence + Retrieve a list of all internal accounts that belong to the platform, as opposed to an individual customer. - **Required documents** - - Company formation and existence documents (certificate of incorporation, - articles of association, etc.) - - Proof of ownership and control structure (organization and ownership - chart, shareholder agreements, operating agreements, register of members, - or certification of controlling person and beneficial owners) - - Proof of address dated within the last 3 months (utility bill, bank - statement, lease agreement, or official correspondence) - - Tax ID or equivalent identifying-number documents - - For non-U.S. beneficial owners — passport plus one additional - government-issued ID - operationId: createVerification + These accounts are created automatically when the platform is configured for each supported currency. They can be used for things like distributing bitcoin rewards to customers, or for other platform-wide purposes. + operationId: listPlatformInternalAccounts tags: - - KYC/KYB Verifications + - Internal Accounts security: - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/VerificationRequest' + parameters: + - name: currency + in: query + description: Filter by currency code + required: false + schema: + 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. + required: false + schema: + $ref: '#/components/schemas/InternalAccountType' responses: '200': - description: | - Verification status returned. Check `verificationStatus` and `errors` to determine next steps. + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Verification' - examples: - missingInfo: - summary: Verification blocked by missing data - value: - id: Verification:019542f5-b3e7-1d02-0000-000000000001 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - verificationStatus: RESOLVE_ERRORS - errors: - - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: MISSING_FIELD - field: customer.address.line1 - reason: Business address line 1 is required - - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: MISSING_PROOF_OF_ADDRESS_DOCUMENT - acceptedDocumentTypes: - - PROOF_OF_ADDRESS - reason: Proof of address document is required - - resourceId: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000002 - type: MISSING_FIELD - field: personalInfo.birthDate - reason: Date of birth is required for beneficial owners - createdAt: '2025-10-03T12:00:00Z' - submitted: - summary: Verification submitted successfully - value: - id: Verification:019542f5-b3e7-1d02-0000-000000000002 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - verificationStatus: IN_PROGRESS - errors: [] - createdAt: '2025-10-03T12:00:00Z' + $ref: '#/components/schemas/PlatformInternalAccountListResponse' '400': description: Bad request - Invalid parameters content: @@ -2208,40 +2156,39 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Customer not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/external-accounts: get: - summary: List verifications + summary: List Customer external accounts description: | - Retrieve a list of verifications with optional filtering by customer ID and status. - operationId: listVerifications + Retrieve a list of external accounts with optional filtering parameters. Returns all + external accounts that match the specified filters. If no filters are provided, returns all external accounts + (paginated). + + External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform. + operationId: listCustomerExternalAccounts tags: - - KYC/KYB Verifications + - External Accounts security: - BasicAuth: [] parameters: - - name: customerId + - name: currency in: query - description: Filter by customer ID + description: Filter by currency code required: false schema: type: string - - name: verificationStatus + - name: customerId in: query - description: Filter by verification status + description: Filter by external accounts associated with a specific customer required: false schema: - $ref: '#/components/schemas/VerificationStatus' + type: string - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -2263,7 +2210,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/VerificationListResponse' + $ref: '#/components/schemas/ExternalAccountListResponse' '400': description: Bad request - Invalid parameters content: @@ -2282,90 +2229,61 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /verifications/{verificationId}: - get: - summary: Get a verification - description: Retrieve details of a specific verification by ID. - operationId: getVerification + post: + summary: Add a new external account + description: Register a new external bank account for a customer. + operationId: createCustomerExternalAccount tags: - - KYC/KYB Verifications + - External Accounts security: - BasicAuth: [] - parameters: - - name: verificationId - in: path - description: Verification ID - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Verification' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Verification not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /transfer-in: - post: - summary: Create a transfer-in request - description: | - Transfer funds from an external account to an internal account for a specific customer. This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the paymentInstructions on the internal account to deposit funds. - operationId: createTransferIn - tags: - - Same-Currency Transfers - security: - - BasicAuth: [] - parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. - schema: - type: string - example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/TransferInRequest' + $ref: '#/components/schemas/ExternalAccountCreateRequest' examples: - transferIn: - summary: Transfer from external to internal account + usBankAccount: + summary: Create external US bank account value: - source: - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - amount: 12550 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + currency: USD + accountInfo: + accountType: USD_ACCOUNT + accountNumber: '12345678901' + routingNumber: '123456789' + bankAccountType: CHECKING + bankName: Chase Bank + platformAccountId: ext_acc_123456 + beneficiary: + beneficiaryType: INDIVIDUAL + fullName: John Doe + birthDate: '1990-01-15' + nationality: US + address: + line1: 123 Main Street + city: San Francisco + state: CA + postalCode: '94105' + country: US + sparkWallet: + summary: Create external Spark wallet + value: + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + currency: BTC + accountInfo: + accountType: SPARK_WALLET + address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu responses: '201': - description: Transfer-in request created successfully + description: External account created successfully content: application/json: schema: - $ref: '#/components/schemas/TransactionOneOf' + $ref: '#/components/schemas/ExternalAccount' '400': - description: Bad request - Invalid parameters + description: Bad request content: application/json: schema: @@ -2376,72 +2294,41 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Customer or account not found + '409': + description: Conflict - External account already exists content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /transfer-out: - post: - summary: Create a transfer-out request - description: | - Transfer funds from an internal account to an external account for a specific customer. - operationId: createTransferOut + /customers/external-accounts/{externalAccountId}: + parameters: + - name: externalAccountId + in: path + description: System-generated unique external account identifier + required: true + schema: + type: string + get: + summary: Get customer external account by ID + description: Retrieve a customer external account by its system-generated ID + operationId: getCustomerExternalAccountById tags: - - Same-Currency Transfers + - External Accounts security: - BasicAuth: [] - parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. - schema: - type: string - example: 550e8400-e29b-41d4-a716-446655440000 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TransferOutRequest' - examples: - transferOut: - summary: Transfer from internal to external account - value: - source: - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - destination: - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - paymentRail: ACH - amount: 12550 - remittanceInformation: '12345' responses: - '201': - description: | - Transfer-out request created successfully. - - For customers outside SCA-regulated regions the transaction proceeds as usual. - - **Strong Customer Authentication (EU):** per-transaction SCA is authorized only on the quote resource (`POST /quotes/{quoteId}/authorize`), and `transfer-out` has no associated quote. `transfer-out` is being deprecated, so SCA-gated EU debits are not offered on this endpoint — use the quote + `execute` flow instead. - content: - application/json: - schema: - $ref: '#/components/schemas/TransactionOneOf' - '400': - description: Bad request - Invalid parameters + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/ExternalAccount' '401': description: Unauthorized content: @@ -2449,7 +2336,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Customer or account not found + description: External account not found content: application/json: schema: @@ -2460,49 +2347,17 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /receiver/uma/{receiverUmaAddress}: - get: - summary: Look up an UMA address for payment - description: | - Lookup a receiving UMA address to determine supported currencies and exchange rates. - This endpoint helps platforms determine what currencies they can send to a given UMA address. - operationId: lookupUma + delete: + summary: Delete customer external account by ID + description: Delete a customer external account by its system-generated ID + operationId: deleteCustomerExternalAccountById tags: - - Cross-Currency Transfers + - External Accounts security: - BasicAuth: [] - parameters: - - name: receiverUmaAddress - in: path - description: UMA address of the intended recipient - required: true - schema: - type: string - - name: senderUmaAddress - in: query - description: UMA address of the sender (optional if customerId is provided) - required: false - schema: - type: string - - name: customerId - in: query - description: System ID of the sender (optional if senderUmaAddress is provided) - required: false - schema: - type: string responses: - '200': - description: Successful lookup - content: - application/json: - schema: - $ref: '#/components/schemas/ReceiverUmaLookupResponse' - '400': - description: Bad request - Missing or invalid parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + '204': + description: External account deleted successfully '401': description: Unauthorized content: @@ -2510,69 +2365,60 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: UMA address not found. The address is well-formed but no receiver exists at the counterparty VASP (`UMA_NOT_FOUND`). + description: External account not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '412': - description: Counterparty doesn't support UMA version - content: - application/json: - schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue - content: - application/json: - schema: - $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /receiver/external-account/{accountId}: + /platform/external-accounts: get: - summary: Look up an external account for payment + summary: List platform external accounts description: | - Lookup an external account by ID to determine supported currencies and exchange rates. - This endpoint helps platforms determine what currencies they can send to a given external account, along with the current estimated exchange rates and minimum and maximum amounts that can be sent. - operationId: lookupExternalAccount + Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer. + + These accounts are used for platform-wide operations such as receiving funds from external sources or managing platform-level payment destinations. + operationId: listPlatformExternalAccounts tags: - - Cross-Currency Transfers + - External Accounts security: - BasicAuth: [] parameters: - - name: accountId - in: path - description: System-generated ID of the external account - required: true + - name: currency + in: query + description: Filter by currency code + required: false schema: type: string - example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - - name: senderUmaAddress + - name: limit in: query - description: UMA address of the sender (optional if customerId is provided) + description: Maximum number of results to return (default 20, max 100) required: false schema: - type: string - - name: customerId + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor in: query - description: System ID of the sender (optional if senderUmaAddress is provided) + description: Cursor for pagination (returned from previous request) required: false schema: type: string responses: '200': - description: Successful lookup + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse' + $ref: '#/components/schemas/ExternalAccountListResponse' '400': - description: Bad request - Missing or invalid parameters + description: Bad request - Invalid parameters content: application/json: schema: @@ -2583,56 +2429,110 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: External account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '412': - description: Counterparty doesn't support UMA version - content: - application/json: - schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue - content: - application/json: - schema: - $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes/{quoteId}: - get: - summary: Get quote by ID - description: | - Retrieve a quote by its ID. If the quote has been settled, it will include - the transaction ID. This allows clients to track the full lifecycle of a payment - from quote creation to settlement. - operationId: getQuoteById - tags: - - Cross-Currency Transfers - security: + post: + summary: Add a new platform external account + description: Register a new external bank account for the platform. + operationId: createPlatformExternalAccount + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PlatformExternalAccountCreateRequest' + examples: + usBankAccount: + summary: Create external US bank account + value: + currency: USD + accountInfo: + accountType: USD_ACCOUNT + accountNumber: '12345678901' + routingNumber: '123456789' + bankAccountType: CHECKING + bankName: Chase Bank + platformAccountId: ext_acc_123456 + beneficiary: + beneficiaryType: INDIVIDUAL + fullName: John Doe + birthDate: '1990-01-15' + nationality: US + address: + line1: 123 Main Street + city: San Francisco + state: CA + postalCode: '94105' + country: US + sparkWallet: + summary: Create external Spark wallet + value: + currency: BTC + accountInfo: + accountType: SPARK_WALLET + address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu + responses: + '201': + description: External account created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '409': + description: Conflict - External account already exists + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /platform/external-accounts/{externalAccountId}: + parameters: + - name: externalAccountId + in: path + description: System-generated unique external account identifier + required: true + schema: + type: string + get: + summary: Get platform external account by ID + description: Retrieve a platform external account by its system-generated ID + operationId: getPlatformExternalAccountById + tags: + - External Accounts + security: - BasicAuth: [] - parameters: - - name: quoteId - in: path - description: ID of the quote to retrieve - required: true - schema: - type: string responses: '200': - description: Quote retrieved successfully + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/ExternalAccount' '401': description: Unauthorized content: @@ -2640,7 +2540,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Quote not found + description: External account not found content: application/json: schema: @@ -2651,132 +2551,60 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes: + delete: + summary: Delete platform external account by ID + description: Delete a platform external account by its system-generated ID + operationId: deletePlatformExternalAccountById + tags: + - External Accounts + security: + - BasicAuth: [] + responses: + '204': + description: External account deleted successfully + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: External account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /beneficial-owners: post: - summary: Create a transfer quote + summary: Create a beneficial owner description: | - Generate a quote for a cross-currency transfer between any combination of accounts - and UMA addresses. This endpoint handles currency exchange and provides the necessary - instructions to execute the transfer. - - **Transfer Types Supported:** - - **Account to Account**: Transfer between internal/external accounts with currency exchange. - - **Account to UMA**: Transfer from an internal account to an UMA address. - - **UMA to Account or UMA to UMA**: This transfer type will only be funded by payment instructions, not from an internal account. - - **Key Features:** - - **Flexible Amount Locking**: Always specify whether you want to lock the sending amount or receiving amount - - **Currency Exchange**: Handles all cross-currency transfers with real-time exchange rates - - **Payment Instructions**: For UMA or customer ID sources, provides banking details needed for execution - - **Important:** If you are transferring funds in the same currency (no exchange required), - use the `/transfer-in` or `/transfer-out` endpoints instead. - operationId: createQuote + Add a beneficial owner, director, or company officer to a business customer. The beneficial owner will go through KYC verification automatically. + operationId: createBeneficialOwner tags: - - Cross-Currency Transfers + - KYC/KYB Verifications security: - BasicAuth: [] - parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. - schema: - type: string - example: requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/QuoteRequest' - examples: - accountToAccount: - summary: Account to Account Transfer - value: - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - lockedCurrencySide: SENDING - lockedCurrencyAmount: 10000 - description: Transfer between accounts, either internal or external. - accountToUma: - summary: Account to UMA Address Transfer - value: - lookupId: LookupRequest:019542f5-b3e7-1d02-0000-000000000009 - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: UMA_ADDRESS - umaAddress: $receiver@uma.domain.com - currency: EUR - lockedCurrencySide: SENDING - lockedCurrencyAmount: 1000 - description: 'Payment for invoice #1234' - realTimeFundingToSparkWallet: - summary: Real-time funding to Spark Wallet as an on-ramp flow. Immediate execution. - value: - source: - sourceType: REALTIME_FUNDING - customerId: Customer:019542f5-b3e7-1d02-0000-000000000009 - currency: USD - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456 - lockedCurrencySide: RECEIVING - lockedCurrencyAmount: 10000 - immediatelyExecute: true - description: Bitcoin reward payout! + $ref: '#/components/schemas/BeneficialOwnerCreateRequest' responses: '201': - description: | - Transfer quote created successfully. The response includes exchange rates, - fees, and transfer details. For transfers involving UMA addresses, payment - instructions are also included for execution through banking systems. - content: - application/json: - schema: - $ref: '#/components/schemas/Quote' - example: - id: Quote:019542f5-b3e7-1d02-0000-000000000006 - status: PENDING - createdAt: '2025-10-03T12:00:00Z' - expiresAt: '2025-10-03T12:05:00Z' - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - sendingCurrency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - receivingCurrency: - code: EUR - name: Euro - symbol: € - decimals: 2 - totalSendingAmount: 10000 - totalReceivingAmount: 9200 - exchangeRate: 0.92 - feesIncluded: 10 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005 - '202': - description: | - Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated. + description: Beneficial owner created successfully content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/BeneficialOwner' '400': - description: Bad request - Missing or invalid parameters + description: Bad request - Invalid parameters content: application/json: schema: @@ -2787,172 +2615,145 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '412': - description: Counterparty doesn't support UMA version + '404': + description: Customer not found content: application/json: schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue - content: - application/json: - schema: - $ref: '#/components/schemas/Error424' + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - '501': - description: Not implemented - content: - application/json: - schema: - $ref: '#/components/schemas/Error501' - /quotes/{quoteId}/execute: - post: - summary: Execute a quote + get: + summary: List beneficial owners description: | - Execute a quote by its ID. This endpoint initiates the transfer between - the source and destination accounts. - - This endpoint can only be used for quotes with a `source` which is either an internal account, - or has direct pull functionality (e.g. ACH pull with an external account). - - When the quote's `source` is an internal account of type `EMBEDDED_WALLET`, - the request must include a `Grid-Wallet-Signature` header. The header value - is the full Grid wallet signature built over the `payloadToSign` value from - the quote's `paymentInstructions[].accountOrWalletInfo` entry with the - session private key of a verified authentication credential on the source - Embedded Wallet. - - Once executed, the quote cannot be cancelled and the transfer will be processed. - operationId: executeQuote + Retrieve a list of beneficial owners for a business customer. + operationId: listBeneficialOwners tags: - - Cross-Currency Transfers + - KYC/KYB Verifications security: - BasicAuth: [] parameters: - - name: quoteId - in: path + - name: customerId + in: query + description: The business customer ID required: true - description: The unique identifier of the quote to execute schema: type: string - example: Quote:019542f5-b3e7-1d02-0000-000000000001 - - name: Idempotency-Key - in: header + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. schema: - type: string - example: - - name: Grid-Wallet-Signature - in: header + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) required: false - description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types. schema: type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - requestBody: - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/ExecuteQuoteRequest' responses: '200': - description: | - Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): - - - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). - - - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`. + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/BeneficialOwnerListResponse' '400': - description: Bad request - Invalid quote ID or quote cannot be confirmed + description: Bad request - Invalid parameters content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Quote not found + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Quote already confirmed, expired, or in invalid state + $ref: '#/components/schemas/Error500' + /beneficial-owners/{beneficialOwnerId}: + get: + summary: Get a beneficial owner + description: Retrieve details of a specific beneficial owner by ID. + operationId: getBeneficialOwner + tags: + - KYC/KYB Verifications + security: + - BasicAuth: [] + parameters: + - name: beneficialOwnerId + in: path + description: Beneficial owner ID + required: true + schema: + type: string + responses: + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/BeneficialOwner' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Beneficial owner not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes/{quoteId}/authorize: - parameters: - - name: quoteId - in: path - description: The unique identifier of the quote whose SCA challenge is being authorized. - required: true - schema: - type: string - example: Quote:019542f5-b3e7-1d02-0000-000000000006 - post: - summary: Authorize a quote's SCA challenge - description: | - Satisfy the Strong Customer Authentication challenge carried by a quote in - `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof. - - This is used for realtime-funding quotes: the quote is returned with an - `scaChallenge` and **without** `paymentInstructions`; once authorized, the - quote advances and its `paymentInstructions` are populated so the customer - can fund the transfer. - - As with all SCA, a quote may require more than one authorization: after - authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the - next `scaChallenge` — authorize that too, repeating until it advances (see - `ScaChallenge`). - - This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. - - In sandbox, the SMS code is always `123456`. - operationId: authorizeQuote + patch: + summary: Update a beneficial owner + description: Update details of a specific beneficial owner. Only provided fields are updated. + operationId: updateBeneficialOwner tags: - - Strong Customer Authentication + - KYC/KYB Verifications security: - BasicAuth: [] + parameters: + - name: beneficialOwnerId + in: path + description: Beneficial owner ID + required: true + schema: + type: string requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/ScaAuthorization' + $ref: '#/components/schemas/BeneficialOwnerUpdateRequest' responses: '200': - description: Challenge authorized; the updated quote is returned. + description: Beneficial owner updated successfully content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/BeneficialOwner' '400': - description: Invalid or expired authorization proof + description: Bad request - Invalid parameters content: application/json: schema: @@ -2964,169 +2765,78 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Quote not found + description: Beneficial owner not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '409': - description: SCA is not required for this customer, or the quote has no pending challenge to authorize. - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes/{quoteId}/authorize/resend: - parameters: - - name: quoteId - in: path - description: The unique identifier of the quote whose SCA challenge code should be re-sent. - required: true - schema: - type: string - example: Quote:019542f5-b3e7-1d02-0000-000000000006 + /documents: post: - summary: Resend a quote's SCA challenge code + summary: Upload a document description: | - Re-send the one-time code for a realtime-funding quote in - `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The - existing challenge is reused — no new challenge is issued, and its - `scaChallenge.expiresAt` is **not** extended; once the challenge is past - `expiresAt` it can no longer be authorized. - - Only meaningful for customers in a region where SCA is required (e.g. EU); - a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409. + Upload a verification document for a customer or beneficial owner. The request must use multipart/form-data with the file in the `file` field and metadata in the remaining fields. - In sandbox, the code is always `123456`. - operationId: resendQuoteScaCode + Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB. + operationId: uploadDocument tags: - - Strong Customer Authentication + - Documents security: - BasicAuth: [] + requestBody: + $ref: '#/components/requestBodies/DocumentUploadRequestBody' responses: - '204': - description: Code re-sent. - '401': - description: Unauthorized + '201': + description: Document uploaded successfully content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Quote not found + $ref: '#/components/schemas/Document' + '400': + description: Bad request - Invalid file type, size, or parameters content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/Error409' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + $ref: '#/components/schemas/Error401' + '404': + description: Document holder not found content: application/json: schema: - $ref: '#/components/schemas/Error429' + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions: get: - summary: List transactions + summary: List documents description: | - Retrieve a paginated list of transactions with optional filtering. - The transactions can be filtered by customer ID, platform customer ID, UMA address, - date range, status, and transaction type. - - Card transactions are included and identified by `type: CARD`. In Sandbox this is how - you discover a `CardTransaction` id after simulating an authorization — list the - transactions, take the card transaction's `id`, and pass it as the `cardTransactionId` - to the clearing and return simulate endpoints. - operationId: listTransactions - tags: - - Transactions + Retrieve a list of documents with optional filtering by document holder. + operationId: listDocuments + tags: + - Documents security: - BasicAuth: [] parameters: - - name: customerId - in: query - description: Filter by system customer ID. To filter to transactions made on behalf of the platform, specify the platform ID as the customer ID. - required: false - schema: - type: string - - name: platformCustomerId - in: query - description: Filter by platform-specific customer ID - required: false - schema: - type: string - - name: accountIdentifier - in: query - description: Filter by account identifier (matches either sender or receiver) - required: false - schema: - type: string - - name: senderAccountIdentifier - in: query - description: Filter by sender account identifier - required: false - schema: - type: string - - name: receiverAccountIdentifier - in: query - description: Filter by receiver account identifier - required: false - schema: - type: string - - name: status - in: query - description: Filter by transaction status - required: false - schema: - $ref: '#/components/schemas/TransactionStatus' - - name: type - in: query - description: Filter by transaction type - required: false - schema: - $ref: '#/components/schemas/TransactionType' - - name: reference - in: query - description: Filter by reference - required: false - schema: - type: string - - name: startDate - in: query - description: Filter by start date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: endDate + - name: documentHolder in: query - description: Filter by end date (inclusive) in ISO 8601 format + description: Filter by document holder ID (Customer or BeneficialOwner) required: false schema: type: string - format: date-time - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -3142,23 +2852,13 @@ paths: required: false schema: type: string - - name: sortOrder - in: query - description: Order to sort results in - required: false - schema: - type: string - enum: - - asc - - desc - default: desc responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/TransactionListResponse' + $ref: '#/components/schemas/DocumentListResponse' '400': description: Bad request - Invalid parameters content: @@ -3177,29 +2877,29 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}: - parameters: - - name: transactionId - in: path - description: Unique identifier of the transaction - required: true - schema: - type: string + /documents/{documentId}: get: - summary: Get transaction by ID - description: Retrieve detailed information about a specific transaction. - operationId: getTransactionById + summary: Get a document by ID + description: Retrieve details and metadata of a specific document by ID. + operationId: getDocument tags: - - Transactions + - Documents security: - BasicAuth: [] + parameters: + - name: documentId + in: path + description: Document ID + required: true + schema: + type: string responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/TransactionOneOf' + $ref: '#/components/schemas/Document' '401': description: Unauthorized content: @@ -3207,7 +2907,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: @@ -3218,38 +2918,33 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/confirm: - post: - summary: Confirm receipt delivery + put: + summary: Replace a document description: | - Confirm that the platform delivered the transaction receipt to its customer. This confirmation is only necessary when the platform is contractually required to send a receipt. If `receiptDeliveryConfirmedAt` is omitted, the confirmation time is set to the current server time. Calling this endpoint again for the same transaction updates the stored confirmation time. - operationId: confirmReceiptDelivery + Replace an existing document with a new file and/or updated metadata. This is useful when a document was rejected and needs to be re-uploaded. The request must use multipart/form-data. + operationId: replaceDocument tags: - - Transactions + - Documents security: - BasicAuth: [] parameters: - - name: transactionId + - name: documentId in: path - description: Unique identifier of the transaction to confirm receipt delivery for + description: Document ID required: true schema: type: string requestBody: - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/ConfirmReceiptDeliveryRequest' + $ref: '#/components/requestBodies/DocumentReplaceRequestBody' responses: '200': - description: Receipt delivery confirmation recorded successfully + description: Document replaced successfully content: application/json: schema: - $ref: '#/components/schemas/TransactionOneOf' + $ref: '#/components/schemas/Document' '400': - description: Bad request - Invalid parameters + description: Bad request - Invalid file type, size, or parameters content: application/json: schema: @@ -3261,7 +2956,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: @@ -3272,43 +2967,25 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/approve: - post: - summary: Approve a pending incoming payment + delete: + summary: Delete a document description: | - Approve a pending incoming payment that was previously acknowledged with a 202 response. - This endpoint allows platforms to asynchronously approve payments after async processing. - operationId: approvePendingPayment + Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable. + operationId: deleteDocument tags: - - Transactions + - Documents security: - BasicAuth: [] parameters: - - name: transactionId + - name: documentId in: path - description: Unique identifier of the transaction to approve + description: Document ID required: true schema: type: string - requestBody: - required: false - content: - application/json: - schema: - $ref: '#/components/schemas/ApprovePaymentRequest' responses: - '200': - description: Payment approved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/IncomingTransaction' - '400': - description: Bad request - Invalid parameters or payment cannot be approved - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + '204': + description: Document deleted successfully '401': description: Unauthorized content: @@ -3316,13 +2993,13 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: Conflict - Payment is not in a pending state or has already been processed or timed out. + description: Conflict - Document cannot be deleted (already submitted for verification) content: application/json: schema: @@ -3333,99 +3010,172 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /transactions/{transactionId}/reject: + /verifications: post: - summary: Reject a pending incoming payment + summary: Submit customer for verification description: | - Reject a pending incoming payment that was previously acknowledged with a 202 response. - This endpoint allows platforms to asynchronously reject payments after additional processing. - operationId: rejectPendingPayment + Trigger KYC (individual) or KYB (business) verification for a customer. + The response indicates whether all required information has been provided. + If data is missing, the `errors` array describes exactly what needs to be + supplied before verification can proceed. + + Call this endpoint again after resolving errors to re-submit. + + ### What to collect for KYB + + Before submitting a `BUSINESS` customer, collect the following via + `POST /customers`, `POST /beneficial-owners`, and `POST /documents`: + + **Business identifying information** + - Entity full legal name + - Doing Business As (DBA) name, if applicable + - Physical address — principal place of business + - Countries of operation + - Identification number — U.S. taxpayer identification number, or, for a + foreign business without one, alternative government-issued documentation + certifying the existence of the business + + **Ownership and control structure** — collected for **one control person** + (an individual with significant responsibility to control, manage, or + direct the legal entity) **and all beneficial owners** (every individual + who owns 25% or more, directly or indirectly). For each, provide: + - Full name + - Date of birth + - Address + - Identification number: + - U.S. persons — SSN or ITIN + - Non-U.S. persons — one or more of: ITIN, passport (with country of + issuance), alien identification card, or another government-issued + photo ID evidencing nationality or residence + + **Required documents** + - Company formation and existence documents (certificate of incorporation, + articles of association, etc.) + - Proof of ownership and control structure (organization and ownership + chart, shareholder agreements, operating agreements, register of members, + or certification of controlling person and beneficial owners) + - Proof of address dated within the last 3 months (utility bill, bank + statement, lease agreement, or official correspondence) + - Tax ID or equivalent identifying-number documents + - For non-U.S. beneficial owners — passport plus one additional + government-issued ID + operationId: createVerification tags: - - Transactions + - KYC/KYB Verifications security: - BasicAuth: [] - parameters: - - name: transactionId - in: path - description: Unique identifier of the transaction to reject - required: true - schema: - type: string requestBody: - required: false + required: true content: application/json: schema: - $ref: '#/components/schemas/RejectPaymentRequest' + $ref: '#/components/schemas/VerificationRequest' responses: '200': - description: Payment rejected successfully - content: - application/json: - schema: - $ref: '#/components/schemas/IncomingTransaction' - '400': - description: Bad request - Invalid parameters or payment cannot be rejected - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized + description: | + Verification status returned. Check `verificationStatus` and `errors` to determine next steps. content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Transaction not found + $ref: '#/components/schemas/Verification' + examples: + missingInfo: + summary: Verification blocked by missing data + value: + id: Verification:019542f5-b3e7-1d02-0000-000000000001 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + verificationStatus: RESOLVE_ERRORS + errors: + - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: MISSING_FIELD + field: customer.address.line1 + reason: Business address line 1 is required + - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: MISSING_PROOF_OF_ADDRESS_DOCUMENT + acceptedDocumentTypes: + - PROOF_OF_ADDRESS + reason: Proof of address document is required + - resourceId: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000002 + type: MISSING_FIELD + field: personalInfo.birthDate + reason: Date of birth is required for beneficial owners + createdAt: '2025-10-03T12:00:00Z' + submitted: + summary: Verification submitted successfully + value: + id: Verification:019542f5-b3e7-1d02-0000-000000000002 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + verificationStatus: IN_PROGRESS + errors: [] + createdAt: '2025-10-03T12:00:00Z' + '400': + description: Bad request - Invalid parameters content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Payment is not in a pending state or has already been processed or timed out. + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /crypto/estimate-withdrawal-fee: - post: - summary: Estimate crypto withdrawal fee + get: + summary: List verifications description: | - Estimate the network and application fees for a cryptocurrency withdrawal from a crypto internal account to an external blockchain address. Use this to show fee information to customers before they initiate a withdrawal. - operationId: estimateCryptoWithdrawalFee + Retrieve a list of verifications with optional filtering by customer ID and status. + operationId: listVerifications tags: - - Cross-Currency Transfers + - KYC/KYB Verifications security: - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeRequest' - examples: - estimateUSDCWithdrawal: - summary: Estimate fee for USDC withdrawal on Solana - value: - internalAccountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - currency: USDC - cryptoNetwork: SOLANA - amount: 1000000 - destinationAddress: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU + parameters: + - name: customerId + in: query + description: Filter by customer ID + required: false + schema: + type: string + - name: verificationStatus + in: query + description: Filter by verification status + required: false + schema: + $ref: '#/components/schemas/VerificationStatus' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string responses: '200': - description: Fee estimation returned successfully + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeResponse' + $ref: '#/components/schemas/VerificationListResponse' '400': description: Bad request - Invalid parameters content: @@ -3438,216 +3188,164 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Internal account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/webhooks/test: - post: - summary: Send a test webhook - description: Send a test webhook to the configured endpoint - operationId: sendTestWebhook + /verifications/{verificationId}: + get: + summary: Get a verification + description: Retrieve details of a specific verification by ID. + operationId: getVerification tags: - - Sandbox + - KYC/KYB Verifications security: - BasicAuth: [] + parameters: + - name: verificationId + in: path + description: Verification ID + required: true + schema: + type: string responses: '200': - description: Webhook delivered successfully - content: - application/json: - schema: - $ref: '#/components/schemas/TestWebhookResponse' - '400': - description: Bad request - Webhook delivery failed + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/Verification' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Verification not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/bulk/csv: + /transfer-in: post: - summary: Upload customers via CSV file + summary: Create a transfer-in request description: | - Upload a CSV file containing customer information for bulk creation. The CSV file should follow - a specific format with required and optional columns based on customer type. - - ### CSV Format - The CSV file should have the following columns: - - Required columns for all customers: - - umaAddress: The customer's UMA address (e.g., $john.doe@uma.domain.com) - - platformCustomerId: Your platform's unique identifier for the customer - - customerType: Either "INDIVIDUAL" or "BUSINESS" - - Required columns for individual customers: - - fullName: Individual's full name - - birthDate: Date of birth in YYYY-MM-DD format - - addressLine1: Street address line 1 - - city: City - - state: State/Province/Region - - postalCode: Postal/ZIP code - - country: Country code (ISO 3166-1 alpha-2) - - Required columns for business customers: - - businessLegalName: Legal name of the business - - addressLine1: Street address line 1 - - city: City - - state: State/Province/Region - - postalCode: Postal/ZIP code - - country: Country code (ISO 3166-1 alpha-2) - - Optional columns for all customers: - - addressLine2: Street address line 2 - - platformAccountId: Your platform's identifier for the bank account - - description: Optional description for the customer - - Optional columns for individual customers: - - email: Customer's email address - - Optional columns for business customers: - - businessRegistrationNumber: Business registration number - - businessTaxId: Tax identification number - - ### Example CSV - ```csv - umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,platformAccountId,businessLegalName - john.doe@uma.domain.com,customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US - acme@uma.domain.com,biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US - ``` - - The upload process is asynchronous and will return a job ID that can be used to track progress. - You can monitor the job status using the `/customers/bulk/jobs/{jobId}` endpoint. - operationId: uploadCustomersCsv + Transfer funds from an external account to an internal account for a specific customer. This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the paymentInstructions on the internal account to deposit funds. + operationId: createTransferIn tags: - - Customers + - Same-Currency Transfers security: - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: - multipart/form-data: + application/json: schema: - type: object - required: - - file - properties: - file: - type: string - format: binary - description: CSV file containing customer information + $ref: '#/components/schemas/TransferInRequest' + examples: + transferIn: + summary: Transfer from external to internal account + value: + source: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + amount: 12550 responses: - '202': - description: CSV upload accepted for processing + '201': + description: Transfer-in request created successfully content: application/json: schema: - $ref: '#/components/schemas/BulkCustomerImportJobAccepted' + $ref: '#/components/schemas/TransactionOneOf' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer or account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/bulk/jobs/{jobId}: - get: - summary: Get bulk import job status + /transfer-out: + post: + summary: Create a transfer-out request description: | - Retrieve the current status and results of a bulk customer import job. This endpoint can be used - to track the progress of both CSV uploads. - - The response includes: - - Overall job status - - Progress statistics - - Detailed error information for failed entries - - Completion timestamp when finished - operationId: getBulkCustomerImportJob + Transfer funds from an internal account to an external account for a specific customer. + operationId: createTransferOut tags: - - Customers + - Same-Currency Transfers security: - BasicAuth: [] parameters: - - name: jobId - in: path - description: ID of the bulk import job to retrieve - required: true + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. schema: type: string - responses: - '200': - description: Job status retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/BulkCustomerImportJob' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Job not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /invitations: - post: - summary: Create an UMA invitation - description: | - Create an UMA invitation from a given platform customer. - operationId: createInvitation - tags: - - Invitations - security: - - BasicAuth: [] + example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/UmaInvitationCreateRequest' + $ref: '#/components/schemas/TransferOutRequest' + examples: + transferOut: + summary: Transfer from internal to external account + value: + source: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + destination: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + paymentRail: ACH + amount: 12550 + remittanceInformation: '12345' responses: '201': - description: Invitation created successfully + description: | + Transfer-out request created successfully. content: application/json: schema: - $ref: '#/components/schemas/UmaInvitation' + $ref: '#/components/schemas/TransactionOneOf' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: @@ -3658,36 +3356,61 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer or account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /invitations/{invitationCode}: + /receiver/uma/{receiverUmaAddress}: get: - summary: Get an UMA invitation by code + summary: Look up an UMA address for payment description: | - Retrieve details about an UMA invitation by its invitation code. - operationId: getInvitation + Lookup a receiving UMA address to determine supported currencies and exchange rates. + This endpoint helps platforms determine what currencies they can send to a given UMA address. + operationId: lookupUma tags: - - Invitations + - Cross-Currency Transfers security: - BasicAuth: [] parameters: - - name: invitationCode + - name: receiverUmaAddress in: path - description: The code of the invitation to get + description: UMA address of the intended recipient required: true schema: type: string + - name: senderUmaAddress + in: query + description: UMA address of the sender (optional if customerId is provided) + required: false + schema: + type: string + - name: customerId + in: query + description: System ID of the sender (optional if senderUmaAddress is provided) + required: false + schema: + type: string responses: '200': - description: Invitation retrieved successfully + description: Successful lookup content: application/json: schema: - $ref: '#/components/schemas/UmaInvitation' + $ref: '#/components/schemas/ReceiverUmaLookupResponse' + '400': + description: Bad request - Missing or invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -3695,56 +3418,69 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Invitation not found + description: UMA address not found. The address is well-formed but no receiver exists at the counterparty VASP (`UMA_NOT_FOUND`). content: application/json: schema: $ref: '#/components/schemas/Error404' + '412': + description: Counterparty doesn't support UMA version + content: + application/json: + schema: + $ref: '#/components/schemas/Error412' + '424': + description: Counterparty issue + content: + application/json: + schema: + $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /invitations/{invitationCode}/claim: - post: - summary: Claim an UMA invitation + /receiver/external-account/{accountId}: + get: + summary: Look up an external account for payment description: | - Claim an UMA invitation by associating it with an invitee UMA address. - - When an invitation is successfully claimed: - 1. The invitation status changes from PENDING to CLAIMED - 2. The invitee UMA address is associated with the invitation - 3. An INVITATION_CLAIMED webhook is triggered to notify the platform that created the invitation - - This endpoint allows customers to accept invitations sent to them by other UMA customers. - operationId: claimInvitation + Lookup an external account by ID to determine supported currencies and exchange rates. + This endpoint helps platforms determine what currencies they can send to a given external account, along with the current estimated exchange rates and minimum and maximum amounts that can be sent. + operationId: lookupExternalAccount tags: - - Invitations + - Cross-Currency Transfers security: - BasicAuth: [] parameters: - - name: invitationCode + - name: accountId in: path - description: The code of the invitation to claim + description: System-generated ID of the external account required: true schema: type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UmaInvitationClaimRequest' + example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + - name: senderUmaAddress + in: query + description: UMA address of the sender (optional if customerId is provided) + required: false + schema: + type: string + - name: customerId + in: query + description: System ID of the sender (optional if senderUmaAddress is provided) + required: false + schema: + type: string responses: '200': - description: Invitation claimed successfully + description: Successful lookup content: application/json: schema: - $ref: '#/components/schemas/UmaInvitation' + $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse' '400': - description: Bad request + description: Bad request - Missing or invalid parameters content: application/json: schema: @@ -3756,69 +3492,63 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Invitation not found + description: External account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '412': + description: Counterparty doesn't support UMA version + content: + application/json: + schema: + $ref: '#/components/schemas/Error412' + '424': + description: Counterparty issue + content: + application/json: + schema: + $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /invitations/{invitationCode}/cancel: - post: - summary: Cancel an UMA invitation + /quotes/{quoteId}: + get: + summary: Get quote by ID description: | - Cancel a pending UMA invitation. Only the inviter or platform can cancel an invitation. - - When an invitation is cancelled: - 1. The invitation status changes from PENDING to CANCELLED - 2. The invitation can no longer be claimed - 3. The invitation URL will show as cancelled when accessed - - Only pending invitations can be cancelled. Attempting to cancel an invitation - that is already claimed, expired, or cancelled will result in an error. - operationId: cancelInvitation + Retrieve a quote by its ID. If the quote has been settled, it will include + the transaction ID. This allows clients to track the full lifecycle of a payment + from quote creation to settlement. + operationId: getQuoteById tags: - - Invitations + - Cross-Currency Transfers security: - BasicAuth: [] parameters: - - name: invitationCode + - name: quoteId in: path - description: The code of the invitation to cancel + description: ID of the quote to retrieve required: true schema: type: string responses: '200': - description: Invitation cancelled successfully - content: - application/json: - schema: - $ref: '#/components/schemas/UmaInvitation' - '400': - description: Bad request - Invitation cannot be cancelled (already claimed, expired, or cancelled) + description: Quote retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/Quote' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Only the platform which created the invitation can cancel it - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Invitation not found + description: Quote not found content: application/json: schema: @@ -3829,32 +3559,132 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/send: + /quotes: post: - summary: Simulate sending funds + summary: Create a transfer quote description: | - Simulate sending funds to the bank account as instructed in the quote. - This endpoint is only for the sandbox environment and will fail for production platforms/keys. - operationId: sandboxSend - tags: - - Sandbox - security: - - BasicAuth: [] + Generate a quote for a cross-currency transfer between any combination of accounts + and UMA addresses. This endpoint handles currency exchange and provides the necessary + instructions to execute the transfer. + + **Transfer Types Supported:** + - **Account to Account**: Transfer between internal/external accounts with currency exchange. + - **Account to UMA**: Transfer from an internal account to an UMA address. + - **UMA to Account or UMA to UMA**: This transfer type will only be funded by payment instructions, not from an internal account. + + **Key Features:** + - **Flexible Amount Locking**: Always specify whether you want to lock the sending amount or receiving amount + - **Currency Exchange**: Handles all cross-currency transfers with real-time exchange rates + - **Payment Instructions**: For UMA or customer ID sources, provides banking details needed for execution + + **Important:** If you are transferring funds in the same currency (no exchange required), + use the `/transfer-in` or `/transfer-out` endpoints instead. + operationId: createQuote + tags: + - Cross-Currency Transfers + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/SandboxSendRequest' + $ref: '#/components/schemas/QuoteRequest' + examples: + accountToAccount: + summary: Account to Account Transfer + value: + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + lockedCurrencySide: SENDING + lockedCurrencyAmount: 10000 + description: Transfer between accounts, either internal or external. + accountToUma: + summary: Account to UMA Address Transfer + value: + lookupId: LookupRequest:019542f5-b3e7-1d02-0000-000000000009 + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: UMA_ADDRESS + umaAddress: $receiver@uma.domain.com + currency: EUR + lockedCurrencySide: SENDING + lockedCurrencyAmount: 1000 + description: 'Payment for invoice #1234' + realTimeFundingToSparkWallet: + summary: Real-time funding to Spark Wallet as an on-ramp flow. Immediate execution. + value: + source: + sourceType: REALTIME_FUNDING + customerId: Customer:019542f5-b3e7-1d02-0000-000000000009 + currency: USD + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456 + lockedCurrencySide: RECEIVING + lockedCurrencyAmount: 10000 + immediatelyExecute: true + description: Bitcoin reward payout! responses: - '200': - description: Funds received successfully + '201': + description: | + Transfer quote created successfully. The response includes exchange rates, + fees, and transfer details. For transfers involving UMA addresses, payment + instructions are also included for execution through banking systems. content: application/json: schema: - $ref: '#/components/schemas/OutgoingTransaction' + $ref: '#/components/schemas/Quote' + example: + id: Quote:019542f5-b3e7-1d02-0000-000000000006 + status: PENDING + createdAt: '2025-10-03T12:00:00Z' + expiresAt: '2025-10-03T12:05:00Z' + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + sendingCurrency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + receivingCurrency: + code: EUR + name: Euro + symbol: € + decimals: 2 + totalSendingAmount: 10000 + totalReceivingAmount: 9200 + exchangeRate: 0.92 + feesIncluded: 10 + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005 + '202': + description: | + Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated. + content: + application/json: + schema: + $ref: '#/components/schemas/Quote' '400': - description: Bad request + description: Bad request - Missing or invalid parameters content: application/json: schema: @@ -3865,121 +3695,172 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token + '412': + description: Counterparty doesn't support UMA version content: application/json: schema: - $ref: '#/components/schemas/Error403' - '404': - description: Quote not found + $ref: '#/components/schemas/Error412' + '424': + description: Counterparty issue content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error424' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/uma/receive: + '501': + description: Not implemented + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /quotes/{quoteId}/execute: post: - summary: Simulate payment send to test receiving an UMA payment + summary: Execute a quote description: | - Simulate sending payment from an sandbox uma address to a platform customer to test payment receive. - This endpoint is only for the sandbox environment and will fail for production platforms/keys. - operationId: sandboxReceive + Execute a quote by its ID. This endpoint initiates the transfer between + the source and destination accounts. + + This endpoint can only be used for quotes with a `source` which is either an internal account, + or has direct pull functionality (e.g. ACH pull with an external account). + + When the quote's `source` is an internal account of type `EMBEDDED_WALLET`, + the request must include a `Grid-Wallet-Signature` header. The header value + is the full Grid wallet signature built over the `payloadToSign` value from + the quote's `paymentInstructions[].accountOrWalletInfo` entry with the + session private key of a verified authentication credential on the source + Embedded Wallet. + + Once executed, the quote cannot be cancelled and the transfer will be processed. + operationId: executeQuote tags: - - Sandbox + - Cross-Currency Transfers security: - BasicAuth: [] + parameters: + - name: quoteId + in: path + required: true + description: The unique identifier of the quote to execute + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000001 + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: + - name: Grid-Wallet-Signature + in: header + required: false + description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/SandboxUmaReceiveRequest' + $ref: '#/components/schemas/ExecuteQuoteRequest' responses: '200': - description: Payment triggered successfully + description: | + Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers): + + - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`). + + - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`. content: application/json: schema: - $ref: '#/components/schemas/IncomingTransaction' + $ref: '#/components/schemas/Quote' '400': - description: Bad request + description: Bad request - Invalid quote ID or quote cannot be confirmed content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`. content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token + '404': + description: Quote not found content: application/json: schema: - $ref: '#/components/schemas/Error403' - '404': - description: Sender or receiver not found + $ref: '#/components/schemas/Error404' + '409': + description: Conflict - Quote already confirmed, expired, or in invalid state content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/internal-accounts/{accountId}/fund: + /quotes/{quoteId}/authorize: + parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge is being authorized. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 post: - summary: Simulate funding an internal account + summary: Authorize a quote's SCA challenge description: | - Simulate receiving funds into an internal account in the sandbox environment. This is useful for testing scenarios where you need to add funds to a customer's or platform's internal account without going through a real bank transfer or following payment instructions. - This endpoint is only for the sandbox environment and will fail for production platforms/keys. - operationId: sandboxFundInternalAccount + Satisfy the Strong Customer Authentication challenge carried by a quote in + `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof. + + This is used for realtime-funding quotes: the quote is returned with an + `scaChallenge` and **without** `paymentInstructions`; once authorized, the + quote advances and its `paymentInstructions` are populated so the customer + can fund the transfer. + + As with all SCA, a quote may require more than one authorization: after + authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the + next `scaChallenge` — authorize that too, repeating until it advances (see + `ScaChallenge`). + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS code is always `123456`. + operationId: authorizeQuote tags: - - Sandbox + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: accountId - in: path - required: true - description: The ID of the internal account to fund - schema: - type: string - example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/SandboxFundRequest' - examples: - fundUSDAccount: - summary: Fund USD internal account with $1,000 - value: - amount: 100000 - fundBTCAccount: - summary: Fund BTC internal account with 0.01 BTC - value: - amount: 1000000 + $ref: '#/components/schemas/ScaAuthorization' responses: '200': - description: Internal account funded successfully + description: Challenge authorized; the updated quote is returned. content: application/json: schema: - $ref: '#/components/schemas/InternalAccount' + $ref: '#/components/schemas/Quote' '400': - description: Bad request + description: Invalid or expired authorization proof content: application/json: schema: @@ -3990,180 +3871,166 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token + '404': + description: Quote not found content: application/json: schema: - $ref: '#/components/schemas/Error403' - '404': - description: Internal account not found + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer, or the quote has no pending challenge to authorize. content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + content: + application/json: + schema: + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /uma-providers: - get: - summary: List available Counterparty Providers + /quotes/{quoteId}/authorize/resend: + parameters: + - name: quoteId + in: path + description: The unique identifier of the quote whose SCA challenge code should be re-sent. + required: true + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000006 + post: + summary: Resend a quote's SCA challenge code description: | - Retrieve a list of available Counterparty Providers. The response includes basic information about each provider, such as its UMA address, name, and supported currencies. - operationId: getAvailableUmaProviders + Re-send the one-time code for a realtime-funding quote in + `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The + existing challenge is reused — no new challenge is issued, and its + `scaChallenge.expiresAt` is **not** extended; once the challenge is past + `expiresAt` it can no longer be authorized. + + Only meaningful for customers in a region where SCA is required (e.g. EU); + a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409. + + In sandbox, the code is always `123456`. + operationId: resendQuoteScaCode tags: - - Available UMA Providers - parameters: - - in: query - name: countryCode - description: The alpha-2 representation of a country, as defined by the ISO 3166-1 standard. - required: false - schema: - type: string - example: US - - in: query - name: currencyCode - description: The ISO 4217 currency code to filter providers by supported currency. - required: false - schema: - type: string - example: USD - - in: query - name: hasBlockedProviders - description: Whether to include providers which are not on your allowlist in the response. By default the response will include blocked providers. - required: false - schema: - type: boolean - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string - - name: sortOrder - in: query - description: Order to sort results in - required: false - schema: - type: string - enum: - - asc - - desc - default: desc + - Strong Customer Authentication security: - BasicAuth: [] responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/UmaProviderListResponse' + '204': + description: Code re-sent. '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /tokens: - post: - summary: Create a new API token - description: Create a new API token to access the Grid APIs. - operationId: createToken - tags: - - API Tokens - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ApiTokenCreateRequest' - responses: - '201': - description: API token created successfully + '404': + description: Quote not found content: application/json: schema: - $ref: '#/components/schemas/ApiToken' - '400': - description: Bad request + $ref: '#/components/schemas/Error404' + '409': + description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey). content: application/json: schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized + $ref: '#/components/schemas/Error409' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header. content: application/json: schema: - $ref: '#/components/schemas/Error401' + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /transactions: get: - summary: List tokens + summary: List transactions description: | - Retrieve a list of API tokens with optional filtering parameters. Returns all tokens that match - the specified filters. If no filters are provided, returns all tokens (paginated). - operationId: listTokens + Retrieve a paginated list of transactions with optional filtering. + The transactions can be filtered by customer ID, platform customer ID, UMA address, + date range, status, and transaction type. + + Card transactions are included and identified by `type: CARD`. In Sandbox this is how + you discover a `CardTransaction` id after simulating an authorization — list the + transactions, take the card transaction's `id`, and pass it as the `cardTransactionId` + to the clearing and return simulate endpoints. + operationId: listTransactions tags: - - API Tokens + - Transactions security: - BasicAuth: [] parameters: - - name: name + - name: customerId in: query - description: Filter by name of the token + description: Filter by system customer ID. To filter to transactions made on behalf of the platform, specify the platform ID as the customer ID. required: false schema: type: string - - name: createdAfter + - name: platformCustomerId in: query - description: Filter customers created after this timestamp (inclusive) + description: Filter by platform-specific customer ID required: false schema: type: string - format: date-time - - name: createdBefore + - name: accountIdentifier in: query - description: Filter customers created before this timestamp (inclusive) + description: Filter by account identifier (matches either sender or receiver) required: false schema: type: string - format: date-time - - name: updatedAfter + - name: senderAccountIdentifier in: query - description: Filter customers updated after this timestamp (inclusive) + description: Filter by sender account identifier + required: false + schema: + type: string + - name: receiverAccountIdentifier + in: query + description: Filter by receiver account identifier + required: false + schema: + type: string + - name: status + in: query + description: Filter by transaction status + required: false + schema: + $ref: '#/components/schemas/TransactionStatus' + - name: type + in: query + description: Filter by transaction type + required: false + schema: + $ref: '#/components/schemas/TransactionType' + - name: reference + in: query + description: Filter by reference + required: false + schema: + type: string + - name: startDate + in: query + description: Filter by start date (inclusive) in ISO 8601 format required: false schema: type: string format: date-time - - name: updatedBefore + - name: endDate in: query - description: Filter customers updated before this timestamp (inclusive) + description: Filter by end date (inclusive) in ISO 8601 format required: false schema: type: string @@ -4183,13 +4050,23 @@ paths: required: false schema: type: string + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/TokenListResponse' + $ref: '#/components/schemas/TransactionListResponse' '400': description: Bad request - Invalid parameters content: @@ -4208,20 +4085,20 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /tokens/{tokenId}: + /transactions/{transactionId}: parameters: - - name: tokenId + - name: transactionId in: path - description: System-generated unique token identifier + description: Unique identifier of the transaction required: true schema: type: string get: - summary: Get API token by ID - description: Retrieve an API token by their system-generated ID - operationId: getTokenById + summary: Get transaction by ID + description: Retrieve detailed information about a specific transaction. + operationId: getTransactionById tags: - - API Tokens + - Transactions security: - BasicAuth: [] responses: @@ -4230,7 +4107,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiToken' + $ref: '#/components/schemas/TransactionOneOf' '401': description: Unauthorized content: @@ -4238,7 +4115,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Token not found + description: Transaction not found content: application/json: schema: @@ -4249,19 +4126,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - delete: - summary: Delete API token by ID - description: Delete an API token by their system-generated ID - operationId: deleteTokenById + /transactions/{transactionId}/confirm: + post: + summary: Confirm receipt delivery + description: | + Confirm that the platform delivered the transaction receipt to its customer. This confirmation is only necessary when the platform is contractually required to send a receipt. If `receiptDeliveryConfirmedAt` is omitted, the confirmation time is set to the current server time. Calling this endpoint again for the same transaction updates the stored confirmation time. + operationId: confirmReceiptDelivery tags: - - API Tokens + - Transactions security: - BasicAuth: [] + parameters: + - name: transactionId + in: path + description: Unique identifier of the transaction to confirm receipt delivery for + required: true + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/ConfirmReceiptDeliveryRequest' responses: - '204': - description: API token deleted successfully + '200': + description: Receipt delivery confirmation recorded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionOneOf' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: @@ -4273,7 +4169,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Token not found + description: Transaction not found content: application/json: schema: @@ -4284,368 +4180,168 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /internal-accounts/{id}: - parameters: - - name: id - in: path - description: The id of the internal account to update. - required: true - schema: - type: string - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - patch: - summary: Update internal account + /transactions/{transactionId}/approve: + post: + summary: Approve a pending incoming payment description: | - Update mutable fields on an internal account. Today this supports updating the wallet privacy setting for an Embedded Wallet internal account. - - Updating wallet privacy is a two-step signed-retry flow: - - 1. Call `PATCH /internal-accounts/{id}` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. - - 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same update fields submitted in step 1. The signed retry returns `200` with the updated internal account. - operationId: updateInternalAccount + Approve a pending incoming payment that was previously acknowledged with a 202 response. + This endpoint allows platforms to asynchronously approve payments after async processing. + operationId: approvePendingPayment tags: - - Internal Accounts + - Transactions security: - BasicAuth: [] parameters: - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + - name: transactionId + in: path + description: Unique identifier of the transaction to approve + required: true schema: type: string - example: Request:019542f5-b3e7-1d02-0000-000000000010 requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/InternalAccountUpdateRequest' - examples: - updateWalletPrivacy: - summary: Update wallet privacy request (both steps) - value: - privateEnabled: true + $ref: '#/components/schemas/ApprovePaymentRequest' responses: '200': - description: Signed retry accepted. Returns the updated internal account. - content: - application/json: - schema: - $ref: '#/components/schemas/InternalAccount' - examples: - enabled: - summary: Wallet privacy enabled - value: - id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - type: EMBEDDED_WALLET - status: ACTIVE - balance: - amount: 12550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - totalBalance: - amount: 12550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - fundingPaymentInstructions: [] - privateEnabled: true - createdAt: '2026-04-08T15:30:00Z' - updatedAt: '2026-04-08T15:35:02Z' - '202': - description: Challenge issued. The response contains `payloadToSign` (which binds the submitted update fields) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + description: Payment approved successfully content: application/json: schema: - $ref: '#/components/schemas/SignedRequestChallenge' - examples: - challenge: - summary: Internal account update challenge - value: - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"encoding":"PAYLOAD_ENCODING_HEXADECIMAL","hashFunction":"HASH_FUNCTION_NO_OP","payload":"9f3b...","signWith":"sp1q..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2"}' - requestId: Request:019542f5-b3e7-1d02-0000-000000000010 - expiresAt: '2026-04-08T15:35:00Z' + $ref: '#/components/schemas/IncomingTransaction' '400': - description: Bad request + description: Bad request - Invalid parameters or payment cannot be approved content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending internal account update challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry body does not match the update fields bound into `payloadToSign` on the initial call. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Internal account not found + description: Transaction not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: Conflict - Payment is not in a pending state or has already been processed or timed out. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /internal-accounts/{id}/export: + /transactions/{transactionId}/reject: post: - summary: Export internal account wallet credentials + summary: Reject a pending incoming payment description: | - Export the wallet credentials of an Embedded Wallet internal account. The returned wallet credentials are HPKE-encrypted to the `clientPublicKey` supplied in the request body. - - Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session): - - 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`. - - 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. - - The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against. - operationId: exportInternalAccount + Reject a pending incoming payment that was previously acknowledged with a 202 response. + This endpoint allows platforms to asynchronously reject payments after additional processing. + operationId: rejectPendingPayment tags: - - Internal Accounts + - Transactions security: - BasicAuth: [] parameters: - - name: id + - name: transactionId in: path - description: The id of the internal account to export. + description: Unique identifier of the transaction to reject required: true schema: type: string - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/InternalAccountExportRequest' - examples: - export: - summary: Export request (both steps) - value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + $ref: '#/components/schemas/RejectPaymentRequest' responses: '200': - description: Signed retry accepted. Returns the encrypted wallet credentials. - content: - application/json: - schema: - $ref: '#/components/schemas/InternalAccountExportResponse' - '202': - description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + description: Payment rejected successfully content: application/json: schema: - $ref: '#/components/schemas/SignedRequestChallenge' + $ref: '#/components/schemas/IncomingTransaction' '400': - description: Bad request + description: Bad request - Invalid parameters or payment cannot be rejected content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending export challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Internal account not found + description: Transaction not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: Conflict - Payment is not in a pending state or has already been processed or timed out. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/credentials: + /crypto/estimate-withdrawal-fee: post: - summary: Create an authentication credential + summary: Estimate crypto withdrawal fee description: | - Register an authentication credential for an Embedded Wallet customer. - - Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered. - - Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. - operationId: createAuthCredential + Estimate the network and application fees for a cryptocurrency withdrawal from a crypto internal account to an external blockchain address. Use this to show fee information to customers before they initiate a withdrawal. + operationId: estimateCryptoWithdrawalFee tags: - - Embedded Wallet Auth + - Cross-Currency Transfers security: - BasicAuth: [] - parameters: - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required on the signed retry. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering a credential; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf' + $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeRequest' examples: - emailOtp: - summary: Add an email OTP credential - value: - type: EMAIL_OTP - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - smsOtp: - summary: Add an SMS OTP credential - value: - type: SMS_OTP - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - oauth: - summary: Add an OAuth credential + estimateUSDCWithdrawal: + summary: Estimate fee for USDC withdrawal on Solana value: - type: OAUTH - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q - passkey: - summary: Add a passkey credential - value: - type: PASSKEY - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - nickname: iPhone Face-ID - challenge: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx - attestation: - credentialId: AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY - clientDataJson: eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0 - attestationObject: o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow - transports: - - internal - - hybrid + internalAccountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + currency: USDC + cryptoNetwork: SOLANA + amount: 1000000 + destinationAddress: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU responses: - '201': - description: Authentication credential created successfully. The body is the created `AuthMethod`. For `EMAIL_OTP`, the nickname is the customer email tied to the internal account; for `SMS_OTP`, it is the customer phone number. OTP responses that trigger a secure OTP challenge carry `otpEncryptionTargetBundle` — the HPKE target bundle the client uses to encrypt the OTP attempt on the subsequent `POST /auth/credentials/{id}/verify`. First-time EMAIL_OTP wallet bootstrap responses may omit that bundle; if it is absent, call `POST /auth/credentials/{id}/challenge` for the new credential to issue a fresh OTP and receive `otpEncryptionTargetBundle` before verifying. For `PASSKEY`, the credential must be authenticated for the first time via `POST /auth/credentials/{id}/challenge` followed by `POST /auth/credentials/{id}/verify` to produce a session — there is no inline authentication challenge on the registration response. - content: - application/json: - schema: - $ref: '#/components/schemas/AuthMethodResponse' - examples: - emailOtp: - summary: Email OTP credential created - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: EMAIL_OTP - nickname: example@lightspark.com - otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - smsOtp: - summary: SMS OTP credential created - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: SMS_OTP - nickname: '+14155550123' - otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - oauth: - summary: OAuth credential created - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: OAUTH - nickname: example@lightspark.com - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - passkey: - summary: Passkey credential created - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: PASSKEY - nickname: iPhone Face-ID - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - '202': - description: Challenge issued. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry. + '200': + description: Fee estimation returned successfully content: application/json: schema: - $ref: '#/components/schemas/AuthSignedRequestChallenge' - examples: - emailOtp: - summary: Additional email OTP credential challenge - value: - type: EMAIL_OTP - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userEmail":"jane@example.com","userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_EMAIL"}' - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' - smsOtp: - summary: Additional SMS OTP credential challenge - value: - type: SMS_OTP - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F...","userPhoneNumber":"+14155550123"},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER"}' - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' - oauth: - summary: Additional OAuth credential challenge - value: - type: OAUTH - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"oauthProviders":[{"oidcToken":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...","providerName":"Google"}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS"}' - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' - passkey: - summary: Additional passkey credential challenge - value: - type: PASSKEY - payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"authenticators":[{"attestation":{"attestationObject":"o2NmbXRk...","clientDataJson":"eyJjaGFsbGVuZ2UiOiJBcktRa...","credentialId":"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY"},"authenticatorName":"iPhone Face-ID","challenge":"ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx","transports":["internal","hybrid"]}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2"}' - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' + $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeResponse' '400': - description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `SMS_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an SMS OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer. + description: Bad request - Invalid parameters content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration. + description: Unauthorized content: application/json: schema: @@ -4662,62 +4358,24 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - get: - summary: List authentication credentials - description: |- - Retrieve all authentication credentials registered on an Embedded Wallet internal account. - - The response is not paginated: an internal account is expected to have a small, bounded number of credentials (typically 1–5), so all results are returned inline. Additional per-credential detail (such as active session expiry) is available on `GET /auth/sessions`. - operationId: listAuthCredentials + /sandbox/webhooks/test: + post: + summary: Send a test webhook + description: Send a test webhook to the configured endpoint + operationId: sendTestWebhook tags: - - Embedded Wallet Auth + - Sandbox security: - BasicAuth: [] - parameters: - - name: accountId - in: query - description: Internal account id whose authentication credentials to list. - required: true - schema: - type: string - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 responses: '200': - description: Authentication credentials registered on the internal account. Returns an empty `data` array when the internal account has no credentials or when `accountId` does not match any internal account visible to the caller. + description: Webhook delivered successfully content: application/json: schema: - $ref: '#/components/schemas/AuthCredentialListResponse' - examples: - multipleCredentials: - summary: Internal account with multiple authentication credentials - value: - data: - - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: EMAIL_OTP - nickname: example@lightspark.com - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:01Z' - - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000004 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: OAUTH - nickname: example@lightspark.com - createdAt: '2026-04-08T15:35:00Z' - updatedAt: '2026-04-08T15:35:00Z' - - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000003 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: PASSKEY - credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew - nickname: iPhone Face-ID - createdAt: '2026-04-09T10:15:00Z' - updatedAt: '2026-04-09T10:15:00Z' - empty: - summary: No credentials registered on the account - value: - data: [] + $ref: '#/components/schemas/TestWebhookResponse' '400': - description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier. + description: Bad request - Webhook delivery failed content: application/json: schema: @@ -4734,282 +4392,168 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/credentials/{id}: - delete: - summary: Revoke an authentication credential + /customers/bulk/csv: + post: + summary: Upload customers via CSV file description: | - Revoke an authentication credential on an Embedded Wallet internal account. + Upload a CSV file containing customer information for bulk creation. The CSV file should follow + a specific format with required and optional columns based on customer type. - Revocation is a two-step flow because it must be authorized by a session on a *different* credential on the same internal account: + ### CSV Format + The CSV file should have the following columns: - 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + Required columns for all customers: + - umaAddress: The customer's UMA address (e.g., $john.doe@uma.domain.com) + - platformCustomerId: Your platform's unique identifier for the customer + - customerType: Either "INDIVIDUAL" or "BUSINESS" - 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + Required columns for individual customers: + - fullName: Individual's full name + - birthDate: Date of birth in YYYY-MM-DD format + - addressLine1: Street address line 1 + - city: City + - state: State/Province/Region + - postalCode: Postal/ZIP code + - country: Country code (ISO 3166-1 alpha-2) - The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it. - operationId: revokeAuthCredential + Required columns for business customers: + - businessLegalName: Legal name of the business + - addressLine1: Street address line 1 + - city: City + - state: State/Province/Region + - postalCode: Postal/ZIP code + - country: Country code (ISO 3166-1 alpha-2) + + Optional columns for all customers: + - addressLine2: Street address line 2 + - platformAccountId: Your platform's identifier for the bank account + - description: Optional description for the customer + + Optional columns for individual customers: + - email: Customer's email address + + Optional columns for business customers: + - businessRegistrationNumber: Business registration number + - businessTaxId: Tax identification number + + ### Example CSV + ```csv + umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,platformAccountId,businessLegalName + john.doe@uma.domain.com,customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US + acme@uma.domain.com,biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US + ``` + + The upload process is asynchronous and will return a job ID that can be used to track progress. + You can monitor the job status using the `/customers/bulk/jobs/{jobId}` endpoint. + operationId: uploadCustomersCsv tags: - - Embedded Wallet Auth + - Customers security: - BasicAuth: [] - parameters: - - name: id - in: path - description: The id of the authentication credential to revoke (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). - required: true - schema: - type: string - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: CSV file containing customer information responses: '202': - description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry. - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSignedRequestChallenge' - '204': - description: Authentication credential revoked successfully. - '400': - description: Bad request. Also returned when the target internal account has only a single authentication credential, which cannot be revoked via this endpoint. + description: CSV upload accepted for processing content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/BulkCustomerImportJobAccepted' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this credential, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Authentication credential not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/credentials/{id}/verify: - post: - summary: Verify an authentication credential + /customers/bulk/jobs/{jobId}: + get: + summary: Get bulk import job status description: | - Complete the verification step for a previously created authentication credential and issue a session. - - For `EMAIL_OTP` and `SMS_OTP` credentials, submit the `encryptedOtpBundle` produced by HPKE-encrypting `{otp_code, public_key}` under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued. The server is a pass-through and never sees the plaintext OTP code. On success the response is `202` with a `payloadToSign` carrying the `verificationToken` bound to the client's TEK public key — sign that token with the matching TEK private key, then retry the same request with the full stamp in `Grid-Wallet-Signature` and the `requestId` echoed in `Request-Id`. The signed retry returns `200` with the issued `AuthSession`. The TEK public key becomes the session API key on successful completion. - In sandbox mode, the OTP flow runs real HPKE end-to-end against a sandbox enclave keypair — clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code (`"000000"`) the user "receives" instead of a real email or SMS delivery. - - For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. + Retrieve the current status and results of a bulk customer import job. This endpoint can be used + to track the progress of both CSV uploads. - On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires. - operationId: verifyAuthCredential + The response includes: + - Overall job status + - Progress statistics + - Detailed error information for failed entries + - Completion timestamp when finished + operationId: getBulkCustomerImportJob tags: - - Embedded Wallet Auth + - Customers security: - BasicAuth: [] parameters: - - name: id + - name: jobId in: path - description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + description: ID of the bulk import job to retrieve required: true schema: type: string - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the TEK (Target Encryption Key) keypair the client generated for this login. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification. Not used by `OAUTH` or `PASSKEY` verification, which complete in a single call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response from this endpoint, echoed back exactly here so the server can correlate the signed retry with the issued challenge. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification; must be paired with `Grid-Wallet-Signature`. For `PASSKEY` verification, the `requestId` issued from `POST /auth/credentials/{id}/challenge` is echoed here instead so the server can correlate the assertion with the pending challenge. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf' - examples: - emailOtp: - summary: Verify an email OTP credential (first leg) - value: - type: EMAIL_OTP - encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' - smsOtp: - summary: Verify an SMS OTP credential (first leg) - value: - type: SMS_OTP - encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' - emailOtpSignedRetry: - summary: Signed retry completing an email OTP verification - description: Same request body as the first leg, plus the `Grid-Wallet-Signature` and `Request-Id` headers carrying the stamp over the `verificationToken` and the `requestId` from the prior `202` response. - value: - type: EMAIL_OTP - encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' - oauth: - summary: Verify an OAuth credential - value: - type: OAUTH - oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 - passkey: - summary: Verify a passkey credential - value: - type: PASSKEY - assertion: - credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew - clientDataJson: eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0 - authenticatorData: PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA - signature: MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6jhd45bDx92wjXKs900 responses: '200': - description: Authentication credential verified and session issued - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSession' - '202': - description: Verification challenge issued. Returned only for OTP credentials, on the first leg of the secure OTP login flow. Build an API-key stamp over `payloadToSign` (the `verificationToken`) with the TEK keypair the client generated for this login, then resubmit the same request with that full stamp as `Grid-Wallet-Signature` and `requestId` echoed as `Request-Id` to receive the issued session on the signed retry. - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSignedRequestChallenge' - examples: - emailOtp: - summary: Email OTP verification challenge (sign and retry) - value: - type: EMAIL_OTP - payloadToSign: eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ.eyJzdWIiOiJUWnk2NkVPa1RGYTd2NkpXZ0VxaVgyZGFXOENXc2pMQzVDVU9aRUlGY3hzIiwiaWF0IjoxNzc5NDA3MjIxLCJleHAiOjE3Nzk0MTA4MjF9.gKX9MWYGkw8Y55bgzsgrRftvUHFruIe8yu0w9Kpjp5qnrZnXcTV71WVoltGPsr015IY_oRTOkIFLHmiGNG9zBw - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' - '400': - description: Bad request + description: Job status retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/BulkCustomerImportJob' '401': - description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP` or `SMS_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. For OTP signed retries, returned when `Grid-Wallet-Signature` is missing, malformed, signed by a public key that does not match the one bound into the `verificationToken`, or when `Request-Id` does not match an unexpired pending verification challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Authentication credential not found + description: Job not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when verification attempts for this credential happen too frequently (for example, repeated bad OTPs or rapid-fire reauthentication retries). Clients should back off and retry after the interval indicated by the `Retry-After` response header. - headers: - Retry-After: - description: Number of seconds to wait before retrying the request. - schema: - type: integer - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/credentials/{id}/challenge: + /invitations: post: - summary: Re-issue an authentication credential challenge + summary: Create an UMA invitation description: | - Re-issue the challenge for an existing authentication credential. - - For `EMAIL_OTP` and `SMS_OTP` credentials, this triggers a new one-time password to the contact on file and returns a fresh `otpEncryptionTargetBundle` for the client to HPKE-encrypt the OTP attempt against. After the user receives the new OTP, build the `encryptedOtpBundle` under the new target bundle and call `POST /auth/credentials/{id}/verify` to begin the secure OTP login flow. - - `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`. - - For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. - operationId: challengeAuthCredential + Create an UMA invitation from a given platform customer. + operationId: createInvitation tags: - - Embedded Wallet Auth + - Invitations security: - BasicAuth: [] - parameters: - - name: id - in: path - description: The id of the authentication credential to re-challenge (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). - required: true - schema: - type: string requestBody: - description: Request body. Required when re-challenging a `PASSKEY` credential (must carry `clientPublicKey`). Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient — the OTP is delivered out-of-band. OAuth credentials do not use this endpoint. - required: false + required: true content: application/json: schema: - $ref: '#/components/schemas/AuthCredentialChallengeRequest' - examples: - passkey: - summary: Re-challenge a passkey credential - value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 - emailOtp: - summary: Re-challenge an email-OTP credential (empty body) - value: {} - smsOtp: - summary: Re-challenge an SMS-OTP credential (empty body) - value: {} + $ref: '#/components/schemas/UmaInvitationCreateRequest' responses: - '200': - description: Challenge re-issued for the authentication credential. For `EMAIL_OTP` and `SMS_OTP` the body is a plain `AuthMethod` and a new OTP has been sent. For `PASSKEY` the body is a `PasskeyAuthChallenge` carrying the passkey `credentialId`, freshly issued `challenge`, `requestId`, and `expiresAt` required to complete reauthentication via `POST /auth/credentials/{id}/verify`. + '201': + description: Invitation created successfully content: application/json: schema: - $ref: '#/components/schemas/AuthCredentialResponseOneOf' - examples: - emailOtp: - summary: Email OTP challenge re-issued - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: EMAIL_OTP - nickname: example@lightspark.com - otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:35:00Z' - passkey: - summary: Passkey reauthentication challenge issued - value: - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: PASSKEY - credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew - nickname: iPhone Face-ID - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:35:00Z' - challenge: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729 - requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 - expiresAt: '2026-04-08T15:35:00Z' + $ref: '#/components/schemas/UmaInvitation' '400': description: Bad request content: @@ -5022,120 +4566,91 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Authentication credential not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '429': - description: Too many requests. Returned with `RATE_LIMITED` when challenge re-issues are requested more frequently than the credential challenge rate limit allows. Clients should back off and retry after the interval indicated by the `Retry-After` response header. - headers: - Retry-After: - description: Number of seconds to wait before retrying the request. - schema: - type: integer - content: - application/json: - schema: - $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/sessions: + /invitations/{invitationCode}: get: - summary: List active sessions - description: |- - Retrieve all active authentication sessions on an Embedded Wallet internal account. A session is created each time a credential is verified via `POST /auth/credentials/{id}/verify`, and remains active until its `expiresAt` passes or it is revoked via `DELETE /auth/sessions/{id}`. - - The response is not paginated: an internal account is expected to have a small, bounded number of concurrent sessions (one per signed-in device, typically 1–4), so all results are returned inline. - operationId: listAuthSessions + summary: Get an UMA invitation by code + description: | + Retrieve details about an UMA invitation by its invitation code. + operationId: getInvitation tags: - - Embedded Wallet Auth + - Invitations security: - BasicAuth: [] parameters: - - name: accountId - in: query - description: Internal account id whose sessions to list. + - name: invitationCode + in: path + description: The code of the invitation to get required: true schema: type: string - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 responses: '200': - description: Active authentication sessions on the internal account. Returns an empty `data` array when the internal account has no active sessions or when `accountId` does not match any internal account visible to the caller. - content: - application/json: - schema: - $ref: '#/components/schemas/SessionListResponse' - '400': - description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier. + description: Invitation retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/UmaInvitation' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '500': + '404': + description: Invitation not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/sessions/{id}: - delete: - summary: Revoke an authentication session + /invitations/{invitationCode}/claim: + post: + summary: Claim an UMA invitation description: | - Revoke an authentication session on an Embedded Wallet internal account. Revocation is a two-step signed-retry flow: - - 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + Claim an UMA invitation by associating it with an invitee UMA address. - 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + When an invitation is successfully claimed: + 1. The invitation status changes from PENDING to CLAIMED + 2. The invitee UMA address is associated with the invitation + 3. An INVITATION_CLAIMED webhook is triggered to notify the platform that created the invitation - Sessions also expire on their own. `404` is returned whenever the `id` does not match an active session — whether the session was never issued, was already revoked by a prior call, or has expired past its `expiresAt`. The response code reflects the resource state, not an error in the client's flow: re-revoking an already-revoked or expired session is safe and idempotent at the user intent level. - operationId: revokeAuthSession + This endpoint allows customers to accept invitations sent to them by other UMA customers. + operationId: claimInvitation tags: - - Embedded Wallet Auth + - Invitations security: - BasicAuth: [] parameters: - - name: id + - name: invitationCode in: path - description: The id of the session to revoke. + description: The code of the invitation to claim required: true schema: type: string - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UmaInvitationClaimRequest' responses: - '202': - description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry. + '200': + description: Invitation claimed successfully content: application/json: schema: - $ref: '#/components/schemas/AuthSignedRequestChallenge' - '204': - description: Session revoked successfully. + $ref: '#/components/schemas/UmaInvitation' '400': description: Bad request content: @@ -5143,13 +4658,13 @@ paths: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this session, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Session not found + description: Invitation not found content: application/json: schema: @@ -5160,101 +4675,58 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/sessions/{id}/refresh: + /invitations/{invitationCode}/cancel: post: - summary: Refresh an authentication session + summary: Cancel an UMA invitation description: | - Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow: - - 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + Cancel a pending UMA invitation. Only the inviter or platform can cancel an invitation. - 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key. + When an invitation is cancelled: + 1. The invitation status changes from PENDING to CANCELLED + 2. The invitation can no longer be claimed + 3. The invitation URL will show as cancelled when accessed - The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead. - operationId: refreshAuthSession + Only pending invitations can be cancelled. Attempting to cancel an invitation + that is already claimed, expired, or cancelled will result in an error. + operationId: cancelInvitation tags: - - Embedded Wallet Auth + - Invitations security: - BasicAuth: [] parameters: - - name: id + - name: invitationCode in: path - description: The id of the active session to refresh. + description: The code of the invitation to cancel required: true schema: type: string - example: Session:019542f5-b3e7-1d02-0000-000000000003 - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the current session API keypair. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in the prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:019542f5-b3e7-1d02-0000-000000000010 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSessionRefreshRequest' - examples: - refresh: - summary: Refresh an active session - value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: - '201': - description: New authentication session created successfully. - content: - application/json: - schema: - $ref: '#/components/schemas/AuthSession' - examples: - session: - summary: Refreshed authentication session - value: - id: Session:019542f5-b3e7-1d02-0000-000000000011 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - type: EMAIL_OTP - encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf - nickname: example@lightspark.com - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:35:00Z' - expiresAt: '2026-04-08T15:50:00Z' - '202': - description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the current session API keypair, then echo `requestId` on the signed retry. + '200': + description: Invitation cancelled successfully content: application/json: schema: - $ref: '#/components/schemas/SignedRequestChallenge' - examples: - challenge: - summary: Session refresh challenge - value: - payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' - requestId: Request:019542f5-b3e7-1d02-0000-000000000010 - expiresAt: '2026-04-08T15:35:00Z' + $ref: '#/components/schemas/UmaInvitation' '400': - description: Bad request + description: Bad request - Invitation cannot be cancelled (already claimed, expired, or cancelled) content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the `BasicAuth` credentials are missing or invalid, when the target session is no longer active and cannot be used for refresh, when the signed retry omits `Grid-Wallet-Signature`, when the provided signature is malformed or does not match the pending refresh challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - Only the platform which created the invitation can cancel it + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' '404': - description: Session not found + description: Invitation not found content: application/json: schema: @@ -5265,80 +4737,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/delegated-keys: + /sandbox/send: post: - summary: Create a delegated signing key + summary: Simulate sending funds description: | - Delegate Spark token-transaction signing authority for a card funding source backed by an Embedded Wallet internal account to a Grid-custodied P-256 API key. Grid uses the requested card and internal account to identify the wallet funding source, generates the keypair server-side, creates an isolated signer identity holding the public key, then policies granting that identity signing and self-revocation authority. The private key is custodied by Grid and never returned. Both activities must be authorized by the wallet owner, so creation is a three-leg signed-retry flow: - - 1. Call `POST /auth/delegated-keys` with no signature headers. Grid generates the delegated keypair and the response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - - 2. Use the session API keypair of a verified credential on the requested Embedded Wallet internal account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The response is a second `202` with a new `payloadToSign`, `requestId`, and `expiresAt`. - - 3. Stamp the new `payloadToSign` with the same session keypair and retry once more with the new `Request-Id`. The signed retry returns `201` with the created `DelegatedKey` in `ACTIVE` status. - - The same request body must be sent on all three legs. A flow abandoned after the second leg leaves the key in `PENDING` status: the signer identity exists but holds no policies, so it cannot sign or revoke itself. Abandoned `PENDING` keys do not block creating another delegated key. After activation, Grid uses the custodied key to authorize signing for the card's Embedded Wallet funding account in place of a session keypair; the platform never handles the key material. - - Each card funding source may have at most one `ACTIVE` delegated key for its Embedded Wallet funding account; revoke the existing active key before creating a new one. A delegated key authorizes raw-payload signing for the wallet and cannot be scoped to amounts or recipients by the public API. Revoke it with `DELETE /auth/delegated-keys/{id}` when no longer needed. - operationId: createDelegatedKey + Simulate sending funds to the bank account as instructed in the quote. + This endpoint is only for the sandbox environment and will fail for production platforms/keys. + operationId: sandboxSend tags: - - Embedded Wallet Auth + - Sandbox security: - BasicAuth: [] - parameters: - - name: Grid-Wallet-Signature - in: header - required: false - description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified credential on the same internal account. Required on the signed retries; ignored on the initial call. - schema: - type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 - - name: Request-Id - in: header - required: false - description: The `requestId` returned in the prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retries; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/DelegatedKeyCreateRequest' - examples: - create: - summary: Delegate signing to a Grid-custodied key - value: - cardId: Card:019542f5-b3e7-1d02-0000-000000000010 - internalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - nickname: Card payments key + $ref: '#/components/schemas/SandboxSendRequest' responses: - '201': - description: Delegated key created and policy granted. The key is `ACTIVE` and Grid may use it to stamp card-payment quote executions for this card funding source's Embedded Wallet funding account. - content: - application/json: - schema: - $ref: '#/components/schemas/DelegatedKey' - examples: - delegatedKey: - summary: Active delegated key - value: - id: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 - cardId: Card:019542f5-b3e7-1d02-0000-000000000010 - fundingSourceId: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011 - accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - publicKey: 02a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90 - nickname: Card payments key - status: ACTIVE - createdAt: '2026-04-08T15:30:01Z' - updatedAt: '2026-04-08T15:30:42Z' - '202': - description: Challenge issued for the next leg. Stamp `payloadToSign` and retry the same request with `Grid-Wallet-Signature` and `Request-Id`. + '200': + description: Funds received successfully content: application/json: schema: - $ref: '#/components/schemas/DelegatedKeySignedRequestChallenge' + $ref: '#/components/schemas/OutgoingTransaction' '400': description: Bad request content: @@ -5346,59 +4768,53 @@ paths: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing on a retry, malformed, or does not match the pending challenge, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Card, card funding source, or Embedded Wallet funding account not found + '403': + description: Forbidden - request was made with a production platform token content: application/json: schema: - $ref: '#/components/schemas/Error404' - '409': - description: An `ACTIVE` delegated key already exists for this card funding source. Revoke it with `DELETE /auth/delegated-keys/{id}` before creating a new one. + $ref: '#/components/schemas/Error403' + '404': + description: Quote not found content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - get: - summary: List delegated signing keys - description: List delegated signing keys for an Embedded Wallet internal account, a card funding source, or both, including `PENDING` keys (user created but policy leg never completed) and `REVOKED` keys. At least one of `accountId` or `fundingSourceId` must be supplied. - operationId: listDelegatedKeys + /sandbox/uma/receive: + post: + summary: Simulate payment send to test receiving an UMA payment + description: | + Simulate sending payment from an sandbox uma address to a platform customer to test payment receive. + This endpoint is only for the sandbox environment and will fail for production platforms/keys. + operationId: sandboxReceive tags: - - Embedded Wallet Auth + - Sandbox security: - BasicAuth: [] - parameters: - - name: accountId - in: query - required: false - description: The id of the internal account whose delegated keys to list. - schema: - type: string - example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - - name: fundingSourceId - in: query - required: false - description: The id of the card funding source whose delegated keys to list. - schema: - type: string - example: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxUmaReceiveRequest' responses: '200': - description: Delegated keys matching the supplied filters. Returns an empty `data` array when no matching delegated keys are visible to the caller. + description: Payment triggered successfully content: application/json: schema: - $ref: '#/components/schemas/DelegatedKeyListResponse' + $ref: '#/components/schemas/IncomingTransaction' '400': description: Bad request content: @@ -5411,44 +4827,85 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Sender or receiver not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /auth/delegated-keys/{id}: - get: - summary: Get a delegated signing key - description: Retrieve a delegated signing key by its system-generated id. - operationId: getDelegatedKeyById + /sandbox/internal-accounts/{accountId}/fund: + post: + summary: Simulate funding an internal account + description: | + Simulate receiving funds into an internal account in the sandbox environment. This is useful for testing scenarios where you need to add funds to a customer's or platform's internal account without going through a real bank transfer or following payment instructions. + This endpoint is only for the sandbox environment and will fail for production platforms/keys. + operationId: sandboxFundInternalAccount tags: - - Embedded Wallet Auth + - Sandbox security: - BasicAuth: [] parameters: - - name: id + - name: accountId in: path - description: The id of the delegated key to retrieve (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys` or `GET /auth/delegated-keys`). required: true + description: The ID of the internal account to fund schema: type: string - example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 + example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxFundRequest' + examples: + fundUSDAccount: + summary: Fund USD internal account with $1,000 + value: + amount: 100000 + fundBTCAccount: + summary: Fund BTC internal account with 0.01 BTC + value: + amount: 1000000 responses: '200': - description: Successful operation + description: Internal account funded successfully content: application/json: schema: - $ref: '#/components/schemas/DelegatedKey' + $ref: '#/components/schemas/InternalAccount' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' '404': - description: Delegated key not found + description: Internal account not found content: application/json: schema: @@ -5459,60 +4916,88 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - delete: - summary: Revoke a delegated signing key + /uma-providers: + get: + summary: List available Counterparty Providers description: | - Revoke an `ACTIVE` delegated signing key. Grid uses the custodied delegated key to authorize deleting its own signer identity. Deleting the identity also removes its API key, after which the delegated key can no longer sign. The response is `204` when revocation completes. - - The underlying signing policies are left in place. Their consensus references the now-deleted signer identity, so they can never authorize anything, and deleting them is unnecessary for correctness or security. - operationId: revokeDelegatedKey + Retrieve a list of available Counterparty Providers. The response includes basic information about each provider, such as its UMA address, name, and supported currencies. + operationId: getAvailableUmaProviders tags: - - Embedded Wallet Auth - security: - - BasicAuth: [] + - Available UMA Providers parameters: - - name: id - in: path - description: The id of the delegated key to revoke (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys`). - required: true + - in: query + name: countryCode + description: The alpha-2 representation of a country, as defined by the ISO 3166-1 standard. + required: false schema: type: string - example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 + example: US + - in: query + name: currencyCode + description: The ISO 4217 currency code to filter providers by supported currency. + required: false + schema: + type: string + example: USD + - in: query + name: hasBlockedProviders + description: Whether to include providers which are not on your allowlist in the response. By default the response will include blocked providers. + required: false + schema: + type: boolean + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc + security: + - BasicAuth: [] responses: - '204': - description: Delegated key revoked. The key can no longer authorize signing. - '400': - description: Bad request. Returned when the delegated key has already been revoked or is not `ACTIVE`. + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/UmaProviderListResponse' '401': - description: Unauthorized. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Delegated key not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents: + /tokens: post: - summary: Create an agent - description: | - Create a new agent with a specified policy. Returns the created agent and a device code that must be redeemed by the agent software to complete installation. - operationId: createAgent + summary: Create a new API token + description: Create a new API token to access the Grid APIs. + operationId: createToken tags: - - Agent Management + - API Tokens security: - BasicAuth: [] requestBody: @@ -5520,14 +5005,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AgentCreateRequest' + $ref: '#/components/schemas/ApiTokenCreateRequest' responses: '201': - description: Agent created successfully + description: API token created successfully content: application/json: schema: - $ref: '#/components/schemas/AgentCreateResponse' + $ref: '#/components/schemas/ApiToken' '400': description: Bad request content: @@ -5547,56 +5032,46 @@ paths: schema: $ref: '#/components/schemas/Error500' get: - summary: List agents - description: Retrieve a paginated list of agents for the authenticated platform. - operationId: listAgents + summary: List tokens + description: | + Retrieve a list of API tokens with optional filtering parameters. Returns all tokens that match + the specified filters. If no filters are provided, returns all tokens (paginated). + operationId: listTokens tags: - - Agent Management + - API Tokens security: - BasicAuth: [] parameters: - - name: customerId + - name: name in: query - description: Filter by customer ID + description: Filter by name of the token required: false schema: type: string - - name: isPaused - in: query - description: Filter by paused status - required: false - schema: - type: boolean - - name: isConnected - in: query - description: Filter by connection status (whether the device code has been redeemed) - required: false - schema: - type: boolean - name: createdAfter in: query - description: Filter agents created after this timestamp (inclusive) + description: Filter customers created after this timestamp (inclusive) required: false schema: type: string format: date-time - name: createdBefore in: query - description: Filter agents created before this timestamp (inclusive) + description: Filter customers created before this timestamp (inclusive) required: false schema: type: string format: date-time - name: updatedAfter in: query - description: Filter agents updated after this timestamp (inclusive) + description: Filter customers updated after this timestamp (inclusive) required: false schema: type: string format: date-time - name: updatedBefore in: query - description: Filter agents updated before this timestamp (inclusive) + description: Filter customers updated before this timestamp (inclusive) required: false schema: type: string @@ -5622,7 +5097,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AgentListResponse' + $ref: '#/components/schemas/TokenListResponse' '400': description: Bad request - Invalid parameters content: @@ -5641,153 +5116,192 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/approvals: + /tokens/{tokenId}: + parameters: + - name: tokenId + in: path + description: System-generated unique token identifier + required: true + schema: + type: string get: - summary: List agent transaction approval requests - description: | - Retrieve a paginated list of agent actions that require platform approval. Filter by `agentId` or `customerId` to scope results to a specific agent or customer. Approve or reject individual actions via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`. - operationId: listAgentApprovals + summary: Get API token by ID + description: Retrieve an API token by their system-generated ID + operationId: getTokenById tags: - - Agent Management + - API Tokens security: - BasicAuth: [] - parameters: - - name: agentId - in: query - description: Filter by agent ID - required: false - schema: - type: string - - name: customerId - in: query - description: Filter by customer ID - required: false - schema: - type: string - - name: startDate - in: query - description: Filter by start date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: endDate - in: query - description: Filter by end date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string - - name: sortOrder - in: query - description: Order to sort results in - required: false - schema: - type: string - enum: - - asc - - desc - default: desc responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/AgentActionListResponse' - '400': - description: Bad request - Invalid parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/ApiToken' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Token not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me: - get: - summary: Get current agent - description: | - Retrieve the authenticated agent's own profile, policy, and current usage. This endpoint is called by the agent software itself using its own credentials (obtained via device code redemption) rather than platform credentials. - operationId: getAgentMe + delete: + summary: Delete API token by ID + description: Delete an API token by their system-generated ID + operationId: deleteTokenById tags: - - Agent Operations + - API Tokens security: - - AgentAuth: [] + - BasicAuth: [] responses: - '200': - description: Successful operation + '204': + description: API token deleted successfully + '400': + description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Agent' + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Token not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/{agentId}: + /internal-accounts/{id}: parameters: - - name: agentId + - name: id in: path - description: System-generated unique agent identifier + description: The id of the internal account to update. required: true schema: type: string - get: - summary: Get agent by ID - description: Retrieve an agent by its system-generated ID. - operationId: getAgentById + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + patch: + summary: Update internal account + description: | + Update mutable fields on an internal account. Today this supports updating the wallet privacy setting for an Embedded Wallet internal account. + + Updating wallet privacy is a two-step signed-retry flow: + + 1. Call `PATCH /internal-accounts/{id}` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same update fields submitted in step 1. The signed retry returns `200` with the updated internal account. + operationId: updateInternalAccount tags: - - Agent Management + - Internal Accounts security: - BasicAuth: [] + parameters: + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountUpdateRequest' + examples: + updateWalletPrivacy: + summary: Update wallet privacy request (both steps) + value: + privateEnabled: true responses: '200': - description: Successful operation + description: Signed retry accepted. Returns the updated internal account. content: application/json: schema: - $ref: '#/components/schemas/Agent' + $ref: '#/components/schemas/InternalAccount' + examples: + enabled: + summary: Wallet privacy enabled + value: + id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + type: EMBEDDED_WALLET + status: ACTIVE + balance: + amount: 12550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + totalBalance: + amount: 12550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + fundingPaymentInstructions: [] + privateEnabled: true + createdAt: '2026-04-08T15:30:00Z' + updatedAt: '2026-04-08T15:35:02Z' + '202': + description: Challenge issued. The response contains `payloadToSign` (which binds the submitted update fields) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/SignedRequestChallenge' + examples: + challenge: + summary: Internal account update challenge + value: + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"encoding":"PAYLOAD_ENCODING_HEXADECIMAL","hashFunction":"HASH_FUNCTION_NO_OP","payload":"9f3b...","signWith":"sp1q..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2"}' + requestId: Request:019542f5-b3e7-1d02-0000-000000000010 + expiresAt: '2026-04-08T15:35:00Z' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending internal account update challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry body does not match the update fields bound into `payloadToSign` on the initial call. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent not found + description: Internal account not found content: application/json: schema: @@ -5798,27 +5312,69 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - patch: - summary: Update agent - description: Update an agent's name or paused state. - operationId: updateAgent + /internal-accounts/{id}/export: + post: + summary: Export internal account wallet credentials + description: | + Export the wallet credentials of an Embedded Wallet internal account. The returned wallet credentials are HPKE-encrypted to the `clientPublicKey` supplied in the request body. + + Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session): + + 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. + + The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against. + operationId: exportInternalAccount tags: - - Agent Management + - Internal Accounts security: - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the internal account to export. + required: true + schema: + type: string + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/AgentUpdateRequest' + $ref: '#/components/schemas/InternalAccountExportRequest' + examples: + export: + summary: Export request (both steps) + value: + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '200': - description: Agent updated successfully + description: Signed retry accepted. Returns the encrypted wallet credentials. content: application/json: schema: - $ref: '#/components/schemas/Agent' + $ref: '#/components/schemas/InternalAccountExportResponse' + '202': + description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/SignedRequestChallenge' '400': description: Bad request content: @@ -5826,13 +5382,13 @@ paths: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending export challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent not found + description: Internal account not found content: application/json: schema: @@ -5843,79 +5399,167 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - delete: - summary: Delete agent - description: Permanently delete an agent. Connected agent software will lose access immediately. - operationId: deleteAgent - tags: - - Agent Management - security: - - BasicAuth: [] - responses: - '204': - description: Agent deleted successfully - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Agent not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /agents/{agentId}/policy: - parameters: - - name: agentId - in: path - description: System-generated unique agent identifier - required: true - schema: - type: string - patch: - summary: Update agent policy + /auth/credentials: + post: + summary: Create an authentication credential description: | - Partially update an agent's policy. Only provided fields will be updated; omitted fields retain their current values. Policy changes take effect immediately. - operationId: updateAgentPolicy + Register an authentication credential for an Embedded Wallet customer. + + Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered. + + Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. + operationId: createAuthCredential tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required on the signed retry. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering a credential; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/AgentPolicyUpdateRequest' + $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf' + examples: + emailOtp: + summary: Add an email OTP credential + value: + type: EMAIL_OTP + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + smsOtp: + summary: Add an SMS OTP credential + value: + type: SMS_OTP + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + oauth: + summary: Add an OAuth credential + value: + type: OAUTH + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q + passkey: + summary: Add a passkey credential + value: + type: PASSKEY + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + nickname: iPhone Face-ID + challenge: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx + attestation: + credentialId: AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY + clientDataJson: eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0 + attestationObject: o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow + transports: + - internal + - hybrid responses: - '200': - description: Agent policy updated successfully + '201': + description: Authentication credential created successfully. The body is the created `AuthMethod`. For `EMAIL_OTP`, the nickname is the customer email tied to the internal account; for `SMS_OTP`, it is the customer phone number. OTP responses that trigger a secure OTP challenge carry `otpEncryptionTargetBundle` — the HPKE target bundle the client uses to encrypt the OTP attempt on the subsequent `POST /auth/credentials/{id}/verify`. First-time EMAIL_OTP wallet bootstrap responses may omit that bundle; if it is absent, call `POST /auth/credentials/{id}/challenge` for the new credential to issue a fresh OTP and receive `otpEncryptionTargetBundle` before verifying. For `PASSKEY`, the credential must be authenticated for the first time via `POST /auth/credentials/{id}/challenge` followed by `POST /auth/credentials/{id}/verify` to produce a session — there is no inline authentication challenge on the registration response. content: application/json: schema: - $ref: '#/components/schemas/Agent' + $ref: '#/components/schemas/AuthMethodResponse' + examples: + emailOtp: + summary: Email OTP credential created + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: EMAIL_OTP + nickname: example@lightspark.com + otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + smsOtp: + summary: SMS OTP credential created + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: SMS_OTP + nickname: '+14155550123' + otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + oauth: + summary: OAuth credential created + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: OAUTH + nickname: example@lightspark.com + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + passkey: + summary: Passkey credential created + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: PASSKEY + nickname: iPhone Face-ID + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + '202': + description: Challenge issued. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthSignedRequestChallenge' + examples: + emailOtp: + summary: Additional email OTP credential challenge + value: + type: EMAIL_OTP + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userEmail":"jane@example.com","userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_EMAIL"}' + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' + smsOtp: + summary: Additional SMS OTP credential challenge + value: + type: SMS_OTP + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F...","userPhoneNumber":"+14155550123"},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER"}' + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' + oauth: + summary: Additional OAuth credential challenge + value: + type: OAUTH + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"oauthProviders":[{"oidcToken":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...","providerName":"Google"}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS"}' + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' + passkey: + summary: Additional passkey credential challenge + value: + type: PASSKEY + payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"authenticators":[{"attestation":{"attestationObject":"o2NmbXRk...","clientDataJson":"eyJjaGFsbGVuZ2UiOiJBcktRa...","credentialId":"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY"},"authenticatorName":"iPhone Face-ID","challenge":"ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx","transports":["internal","hybrid"]}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2"}' + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' '400': - description: Bad request + description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `SMS_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an SMS OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer. content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent not found + description: Internal account not found content: application/json: schema: @@ -5926,32 +5570,62 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/{agentId}/device-codes: - parameters: - - name: agentId - in: path - description: System-generated unique agent identifier - required: true - schema: - type: string - post: - summary: Regenerate a device code - description: | - Generate a new device code for an existing agent. Use this when the original device code has expired before being redeemed, or when the agent software needs to be reinstalled. Any previously issued unredeemed device codes for this agent are invalidated. - operationId: regenerateAgentDeviceCode + get: + summary: List authentication credentials + description: |- + Retrieve all authentication credentials registered on an Embedded Wallet internal account. + + The response is not paginated: an internal account is expected to have a small, bounded number of credentials (typically 1–5), so all results are returned inline. Additional per-credential detail (such as active session expiry) is available on `GET /auth/sessions`. + operationId: listAuthCredentials tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: accountId + in: query + description: Internal account id whose authentication credentials to list. + required: true + schema: + type: string + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 responses: - '201': - description: New device code generated successfully + '200': + description: Authentication credentials registered on the internal account. Returns an empty `data` array when the internal account has no credentials or when `accountId` does not match any internal account visible to the caller. content: application/json: schema: - $ref: '#/components/schemas/AgentDeviceCode' + $ref: '#/components/schemas/AuthCredentialListResponse' + examples: + multipleCredentials: + summary: Internal account with multiple authentication credentials + value: + data: + - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: EMAIL_OTP + nickname: example@lightspark.com + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:01Z' + - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000004 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: OAUTH + nickname: example@lightspark.com + createdAt: '2026-04-08T15:35:00Z' + updatedAt: '2026-04-08T15:35:00Z' + - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000003 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: PASSKEY + credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew + nickname: iPhone Face-ID + createdAt: '2026-04-09T10:15:00Z' + updatedAt: '2026-04-09T10:15:00Z' + empty: + summary: No credentials registered on the account + value: + data: [] '400': - description: Bad request + description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier. content: application/json: schema: @@ -5962,332 +5636,352 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: Agent not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Agent already has an active connection and cannot regenerate a device code - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/{agentId}/actions/{actionId}/approve: - parameters: - - name: agentId - in: path - description: System-generated unique agent identifier - required: true - schema: - type: string - - name: actionId - in: path - description: Unique identifier of the agent action to approve - required: true - schema: - type: string - post: - summary: Approve an agent action + /auth/credentials/{id}: + delete: + summary: Revoke an authentication credential description: | - Approve a pending agent action, allowing Grid to proceed with execution. The action must have status `PENDING_APPROVAL`. Once approved, Grid executes the underlying operation (quote execution or transfer) and the action transitions to `APPROVED`. - For `EXECUTE_QUOTE` actions, note that the underlying quote may have expired between submission and approval — in that case the action will transition to `FAILED` instead. - This endpoint is called by the platform's backend using platform credentials, not by the agent itself. - operationId: approveAgentAction + Revoke an authentication credential on an Embedded Wallet internal account. + + Revocation is a two-step flow because it must be authorized by a session on a *different* credential on the same internal account: + + 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + + The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it. + operationId: revokeAuthCredential tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the authentication credential to revoke (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + required: true + schema: + type: string + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 responses: - '200': - description: Action approved successfully. Returns the updated AgentAction. + '202': + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry. content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/AuthSignedRequestChallenge' + '204': + description: Authentication credential revoked successfully. '400': - description: Bad request - Action cannot be approved + description: Bad request. Also returned when the target internal account has only a single authentication credential, which cannot be revoked via this endpoint. content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this credential, or when the `Request-Id` does not match an unexpired pending challenge. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent or action not found + description: Authentication credential not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Action is not pending approval or has already been processed - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/{agentId}/actions/{actionId}/reject: - parameters: - - name: agentId - in: path - description: System-generated unique agent identifier - required: true - schema: - type: string - - name: actionId - in: path - description: Unique identifier of the agent action to reject - required: true - schema: - type: string + /auth/credentials/{id}/verify: post: - summary: Reject an agent action + summary: Verify an authentication credential description: | - Reject a pending agent action, preventing execution. The action must have status `PENDING_APPROVAL`. Once rejected, the action transitions to `REJECTED` and the underlying operation is not executed. - This endpoint is called by the platform's backend using platform credentials, not by the agent itself. - operationId: rejectAgentAction + Complete the verification step for a previously created authentication credential and issue a session. + + For `EMAIL_OTP` and `SMS_OTP` credentials, submit the `encryptedOtpBundle` produced by HPKE-encrypting `{otp_code, public_key}` under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued. The server is a pass-through and never sees the plaintext OTP code. On success the response is `202` with a `payloadToSign` carrying the `verificationToken` bound to the client's TEK public key — sign that token with the matching TEK private key, then retry the same request with the full stamp in `Grid-Wallet-Signature` and the `requestId` echoed in `Request-Id`. The signed retry returns `200` with the issued `AuthSession`. The TEK public key becomes the session API key on successful completion. + In sandbox mode, the OTP flow runs real HPKE end-to-end against a sandbox enclave keypair — clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code (`"000000"`) the user "receives" instead of a real email or SMS delivery. + + For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. + + On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires. + operationId: verifyAuthCredential tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + required: true + schema: + type: string + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the TEK (Target Encryption Key) keypair the client generated for this login. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification. Not used by `OAUTH` or `PASSKEY` verification, which complete in a single call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response from this endpoint, echoed back exactly here so the server can correlate the signed retry with the issued challenge. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification; must be paired with `Grid-Wallet-Signature`. For `PASSKEY` verification, the `requestId` issued from `POST /auth/credentials/{id}/challenge` is echoed here instead so the server can correlate the assertion with the pending challenge. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: - required: false + required: true content: application/json: schema: - $ref: '#/components/schemas/AgentActionRejectRequest' + $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf' + examples: + emailOtp: + summary: Verify an email OTP credential (first leg) + value: + type: EMAIL_OTP + encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' + smsOtp: + summary: Verify an SMS OTP credential (first leg) + value: + type: SMS_OTP + encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' + emailOtpSignedRetry: + summary: Signed retry completing an email OTP verification + description: Same request body as the first leg, plus the `Grid-Wallet-Signature` and `Request-Id` headers carrying the stamp over the `verificationToken` and the `requestId` from the prior `202` response. + value: + type: EMAIL_OTP + encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}' + oauth: + summary: Verify an OAuth credential + value: + type: OAUTH + oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + passkey: + summary: Verify a passkey credential + value: + type: PASSKEY + assertion: + credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew + clientDataJson: eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0 + authenticatorData: PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA + signature: MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6jhd45bDx92wjXKs900 responses: '200': - description: Action rejected successfully. Returns the updated AgentAction. + description: Authentication credential verified and session issued content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/AuthSession' + '202': + description: Verification challenge issued. Returned only for OTP credentials, on the first leg of the secure OTP login flow. Build an API-key stamp over `payloadToSign` (the `verificationToken`) with the TEK keypair the client generated for this login, then resubmit the same request with that full stamp as `Grid-Wallet-Signature` and `requestId` echoed as `Request-Id` to receive the issued session on the signed retry. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthSignedRequestChallenge' + examples: + emailOtp: + summary: Email OTP verification challenge (sign and retry) + value: + type: EMAIL_OTP + payloadToSign: eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ.eyJzdWIiOiJUWnk2NkVPa1RGYTd2NkpXZ0VxaVgyZGFXOENXc2pMQzVDVU9aRUlGY3hzIiwiaWF0IjoxNzc5NDA3MjIxLCJleHAiOjE3Nzk0MTA4MjF9.gKX9MWYGkw8Y55bgzsgrRftvUHFruIe8yu0w9Kpjp5qnrZnXcTV71WVoltGPsr015IY_oRTOkIFLHmiGNG9zBw + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' '400': - description: Bad request - Action cannot be rejected + description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP` or `SMS_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. For OTP signed retries, returned when `Grid-Wallet-Signature` is missing, malformed, signed by a public key that does not match the one bound into the `verificationToken`, or when `Request-Id` does not match an unexpired pending verification challenge. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Agent or action not found + description: Authentication credential not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Action is not pending approval or has already been processed + '429': + description: Too many requests. Returned with `RATE_LIMITED` when verification attempts for this credential happen too frequently (for example, repeated bad OTPs or rapid-fire reauthentication retries). Clients should back off and retry after the interval indicated by the `Retry-After` response header. + headers: + Retry-After: + description: Number of seconds to wait before retrying the request. + schema: + type: integer content: application/json: schema: - $ref: '#/components/schemas/Error409' + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/device-codes/{code}/status: - parameters: - - name: code - in: path - description: The device code to check - required: true - schema: - type: string - get: - summary: Get device code status + /auth/credentials/{id}/challenge: + post: + summary: Re-issue an authentication credential challenge description: | - Check whether a device code has been redeemed. Use this to poll for agent installation completion after creating an agent. - operationId: getAgentDeviceCodeStatus + Re-issue the challenge for an existing authentication credential. + + For `EMAIL_OTP` and `SMS_OTP` credentials, this triggers a new one-time password to the contact on file and returns a fresh `otpEncryptionTargetBundle` for the client to HPKE-encrypt the OTP attempt against. After the user receives the new OTP, build the `encryptedOtpBundle` under the new target bundle and call `POST /auth/credentials/{id}/verify` to begin the secure OTP login flow. + + `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`. + + For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. + operationId: challengeAuthCredential tags: - - Agent Management + - Embedded Wallet Auth security: - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the authentication credential to re-challenge (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`). + required: true + schema: + type: string + requestBody: + description: Request body. Required when re-challenging a `PASSKEY` credential (must carry `clientPublicKey`). Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient — the OTP is delivered out-of-band. OAuth credentials do not use this endpoint. + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/AuthCredentialChallengeRequest' + examples: + passkey: + summary: Re-challenge a passkey credential + value: + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + emailOtp: + summary: Re-challenge an email-OTP credential (empty body) + value: {} + smsOtp: + summary: Re-challenge an SMS-OTP credential (empty body) + value: {} responses: '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/AgentDeviceCodeStatusResponse' - '401': - description: Unauthorized + description: Challenge re-issued for the authentication credential. For `EMAIL_OTP` and `SMS_OTP` the body is a plain `AuthMethod` and a new OTP has been sent. For `PASSKEY` the body is a `PasskeyAuthChallenge` carrying the passkey `credentialId`, freshly issued `challenge`, `requestId`, and `expiresAt` required to complete reauthentication via `POST /auth/credentials/{id}/verify`. content: application/json: schema: - $ref: '#/components/schemas/Error401' - '404': - description: Device code not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error + $ref: '#/components/schemas/AuthCredentialResponseOneOf' + examples: + emailOtp: + summary: Email OTP challenge re-issued + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: EMAIL_OTP + nickname: example@lightspark.com + otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}''' + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:35:00Z' + passkey: + summary: Passkey reauthentication challenge issued + value: + id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: PASSKEY + credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew + nickname: iPhone Face-ID + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:35:00Z' + challenge: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729 + requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' + '400': + description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Error500' - /agents/device-codes/{code}/redeem: - parameters: - - name: code - in: path - description: The device code to redeem - required: true - schema: - type: string - post: - summary: Redeem device code - description: | - Redeem a device code to obtain agent credentials. This endpoint is called by the agent software during installation. On success, returns a Bearer access token that the agent uses for all subsequent API calls. The token is returned only once and must be stored securely. - This endpoint does not require platform authentication — the device code itself serves as proof of authorization. - operationId: redeemAgentDeviceCode - tags: - - Agent Management - security: [] - responses: - '200': - description: Device code redeemed successfully + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse' - '400': - description: Bad request (e.g., code already redeemed or expired) + $ref: '#/components/schemas/Error401' + '404': + description: Authentication credential not found content: application/json: schema: - $ref: '#/components/schemas/Error400' - '404': - description: Device code not found + $ref: '#/components/schemas/Error404' + '429': + description: Too many requests. Returned with `RATE_LIMITED` when challenge re-issues are requested more frequently than the credential challenge rate limit allows. Clients should back off and retry after the interval indicated by the `Retry-After` response header. + headers: + Retry-After: + description: Number of seconds to wait before retrying the request. + schema: + type: integer content: application/json: schema: - $ref: '#/components/schemas/Error404' + $ref: '#/components/schemas/Error429' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/transactions: + /auth/sessions: get: - summary: List agent transactions - description: | - Retrieve a paginated list of transactions for the authenticated agent's customer. Results are automatically scoped to the agent's associated customer — no customer filter is needed or accepted. - operationId: agentListTransactions + summary: List active sessions + description: |- + Retrieve all active authentication sessions on an Embedded Wallet internal account. A session is created each time a credential is verified via `POST /auth/credentials/{id}/verify`, and remains active until its `expiresAt` passes or it is revoked via `DELETE /auth/sessions/{id}`. + + The response is not paginated: an internal account is expected to have a small, bounded number of concurrent sessions (one per signed-in device, typically 1–4), so all results are returned inline. + operationId: listAuthSessions tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: accountIdentifier - in: query - description: Filter by account identifier (matches either sender or receiver) - required: false - schema: - type: string - - name: senderAccountIdentifier - in: query - description: Filter by sender account identifier - required: false - schema: - type: string - - name: receiverAccountIdentifier - in: query - description: Filter by receiver account identifier - required: false - schema: - type: string - - name: status - in: query - description: Filter by transaction status - required: false - schema: - $ref: '#/components/schemas/TransactionStatus' - - name: type - in: query - description: Filter by transaction type - required: false - schema: - $ref: '#/components/schemas/TransactionType' - - name: reference - in: query - description: Filter by reference - required: false - schema: - type: string - - name: startDate - in: query - description: Filter by start date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: endDate - in: query - description: Filter by end date (inclusive) in ISO 8601 format - required: false - schema: - type: string - format: date-time - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string - - name: sortOrder + - name: accountId in: query - description: Order to sort results in - required: false + description: Internal account id whose sessions to list. + required: true schema: type: string - enum: - - asc - - desc - default: desc + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 responses: '200': - description: Successful operation + description: Active authentication sessions on the internal account. Returns an empty `data` array when the internal account has no active sessions or when `accountId` does not match any internal account visible to the caller. content: application/json: schema: - $ref: '#/components/schemas/TransactionListResponse' + $ref: '#/components/schemas/SessionListResponse' '400': - description: Bad request - Invalid parameters + description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier. content: application/json: schema: @@ -6304,38 +5998,66 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/transactions/{transactionId}: - parameters: - - name: transactionId - in: path - description: Unique identifier of the transaction - required: true - schema: - type: string - get: - summary: Get agent transaction by ID + /auth/sessions/{id}: + delete: + summary: Revoke an authentication session description: | - Retrieve a specific transaction belonging to the authenticated agent's customer. Returns 404 if the transaction exists but belongs to a different customer. - operationId: agentGetTransaction + Revoke an authentication session on an Embedded Wallet internal account. Revocation is a two-step signed-retry flow: + + 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`. + + Sessions also expire on their own. `404` is returned whenever the `id` does not match an active session — whether the session was never issued, was already revoked by a prior call, or has expired past its `expiresAt`. The response code reflects the resource state, not an error in the client's flow: re-revoking an already-revoked or expired session is safe and idempotent at the user intent level. + operationId: revokeAuthSession tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the session to revoke. + required: true + schema: + type: string + - name: Grid-Wallet-Signature + in: header + required: false + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 responses: - '200': - description: Successful operation + '202': + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry. content: application/json: schema: - $ref: '#/components/schemas/TransactionOneOf' + $ref: '#/components/schemas/AuthSignedRequestChallenge' + '204': + description: Session revoked successfully. + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this session, or when the `Request-Id` does not match an unexpired pending challenge. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Session not found content: application/json: schema: @@ -6346,107 +6068,101 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/quotes: + /auth/sessions/{id}/refresh: post: - summary: Create a transfer quote + summary: Refresh an authentication session description: | - Generate a quote for a cross-currency transfer on behalf of the authenticated agent's customer. Accounts referenced in the request must belong to the agent's customer. Requires the CREATE_QUOTES permission in the agent's policy. - If the agent's defaultExecutionMode is APPROVAL_REQUIRED, or the quote amount exceeds the agent's approvalThresholds, the resulting transaction will require explicit approval before funds move. - operationId: agentCreateQuote + Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow: + + 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key. + + The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead. + operationId: refreshAuthSession tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: Idempotency-Key + - name: id + in: path + description: The id of the active session to refresh. + required: true + schema: + type: string + example: Session:019542f5-b3e7-1d02-0000-000000000003 + - name: Grid-Wallet-Signature in: header required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + description: Full API-key stamp built over the prior `payloadToSign` with the current session API keypair. Required on the signed retry; ignored on the initial call. schema: type: string - example: + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id + in: header + required: false + description: The `requestId` returned in the prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: Request:019542f5-b3e7-1d02-0000-000000000010 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/QuoteRequest' + $ref: '#/components/schemas/AuthSessionRefreshRequest' + examples: + refresh: + summary: Refresh an active session + value: + clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '201': - description: Transfer quote created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Quote' - '400': - description: Bad request - Missing or invalid parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Agent policy does not permit this operation - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' - '412': - description: Counterparty doesn't support UMA version - content: - application/json: - schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue + description: New authentication session created successfully. content: application/json: schema: - $ref: '#/components/schemas/Error424' - '500': - description: Internal service error + $ref: '#/components/schemas/AuthSession' + examples: + session: + summary: Refreshed authentication session + value: + id: Session:019542f5-b3e7-1d02-0000-000000000011 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + type: EMAIL_OTP + encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf + nickname: example@lightspark.com + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:35:00Z' + expiresAt: '2026-04-08T15:50:00Z' + '202': + description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the current session API keypair, then echo `requestId` on the signed retry. content: application/json: schema: - $ref: '#/components/schemas/Error500' - /agents/me/quotes/{quoteId}: - parameters: - - name: quoteId - in: path - description: ID of the quote to retrieve - required: true - schema: - type: string - get: - summary: Get agent quote by ID - description: | - Retrieve a quote created by the authenticated agent. Returns 404 if the quote exists but was not created by this agent. - operationId: agentGetQuote - tags: - - Agent Operations - security: - - AgentAuth: [] - responses: - '200': - description: Quote retrieved successfully + $ref: '#/components/schemas/SignedRequestChallenge' + examples: + challenge: + summary: Session refresh challenge + value: + payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' + requestId: Request:019542f5-b3e7-1d02-0000-000000000010 + expiresAt: '2026-04-08T15:35:00Z' + '400': + description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Quote' + $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Returned when the `BasicAuth` credentials are missing or invalid, when the target session is no longer active and cannot be used for refresh, when the signed retry omits `Grid-Wallet-Signature`, when the provided signature is malformed or does not match the pending refresh challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call. content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Quote not found + description: Session not found content: application/json: schema: @@ -6457,75 +6173,100 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/quotes/{quoteId}/execute: - parameters: - - name: quoteId - in: path - required: true - description: The unique identifier of the quote to execute - schema: - type: string - example: Quote:019542f5-b3e7-1d02-0000-000000000001 + /auth/delegated-keys: post: - summary: Execute a quote + summary: Create a delegated signing key description: | - Execute a quote created by the authenticated agent. Requires the EXECUTE_QUOTES permission in the agent's policy. - If the agent's policy requires approval for this amount (based on execution mode or approval thresholds), the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. - Once executed, the quote cannot be cancelled. - operationId: agentExecuteQuote + Delegate Spark token-transaction signing authority for a card funding source backed by an Embedded Wallet internal account to a Grid-custodied P-256 API key. Grid uses the requested card and internal account to identify the wallet funding source, generates the keypair server-side, creates an isolated signer identity holding the public key, then policies granting that identity signing and self-revocation authority. The private key is custodied by Grid and never returned. Both activities must be authorized by the wallet owner, so creation is a three-leg signed-retry flow: + + 1. Call `POST /auth/delegated-keys` with no signature headers. Grid generates the delegated keypair and the response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Use the session API keypair of a verified credential on the requested Embedded Wallet internal account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The response is a second `202` with a new `payloadToSign`, `requestId`, and `expiresAt`. + + 3. Stamp the new `payloadToSign` with the same session keypair and retry once more with the new `Request-Id`. The signed retry returns `201` with the created `DelegatedKey` in `ACTIVE` status. + + The same request body must be sent on all three legs. A flow abandoned after the second leg leaves the key in `PENDING` status: the signer identity exists but holds no policies, so it cannot sign or revoke itself. Abandoned `PENDING` keys do not block creating another delegated key. After activation, Grid uses the custodied key to authorize signing for the card's Embedded Wallet funding account in place of a session keypair; the platform never handles the key material. + + Each card funding source may have at most one `ACTIVE` delegated key for its Embedded Wallet funding account; revoke the existing active key before creating a new one. A delegated key authorizes raw-payload signing for the wallet and cannot be scoped to amounts or recipients by the public API. Revoke it with `DELETE /auth/delegated-keys/{id}` when no longer needed. + operationId: createDelegatedKey tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: Idempotency-Key + - name: Grid-Wallet-Signature in: header required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified credential on the same internal account. Required on the signed retries; ignored on the initial call. schema: type: string - example: - - name: Grid-Wallet-Signature + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + - name: Request-Id in: header required: false - description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types. + description: The `requestId` returned in the prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retries; must be paired with `Grid-Wallet-Signature`. schema: type: string - example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DelegatedKeyCreateRequest' + examples: + create: + summary: Delegate signing to a Grid-custodied key + value: + cardId: Card:019542f5-b3e7-1d02-0000-000000000010 + internalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + nickname: Card payments key responses: - '200': - description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.' + '201': + description: Delegated key created and policy granted. The key is `ACTIVE` and Grid may use it to stamp card-payment quote executions for this card funding source's Embedded Wallet funding account. content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/DelegatedKey' + examples: + delegatedKey: + summary: Active delegated key + value: + id: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 + cardId: Card:019542f5-b3e7-1d02-0000-000000000010 + fundingSourceId: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011 + accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + publicKey: 02a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90 + nickname: Card payments key + status: ACTIVE + createdAt: '2026-04-08T15:30:01Z' + updatedAt: '2026-04-08T15:30:42Z' + '202': + description: Challenge issued for the next leg. Stamp `payloadToSign` and retry the same request with `Grid-Wallet-Signature` and `Request-Id`. + content: + application/json: + schema: + $ref: '#/components/schemas/DelegatedKeySignedRequestChallenge' '400': - description: Bad request - Quote cannot be executed + description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`. + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing on a retry, malformed, or does not match the pending challenge, or when the `Request-Id` does not match an unexpired pending challenge. content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Agent policy does not permit this operation or spending limit exceeded - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Quote not found + description: Card, card funding source, or Embedded Wallet funding account not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: Conflict - Quote already executed, expired, or in invalid state + description: An `ACTIVE` delegated key already exists for this card funding source. Revoke it with `DELETE /auth/delegated-keys/{id}` before creating a new one. content: application/json: schema: @@ -6536,81 +6277,78 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/actions: get: - summary: List agent's own actions - description: | - Retrieve a paginated list of actions submitted by the authenticated agent. Use this to poll for approval decisions after submitting an action that requires approval. - operationId: agentListActions + summary: List delegated signing keys + description: List delegated signing keys for an Embedded Wallet internal account, a card funding source, or both, including `PENDING` keys (user created but policy leg never completed) and `REVOKED` keys. At least one of `accountId` or `fundingSourceId` must be supplied. + operationId: listDelegatedKeys tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: status - in: query - description: Filter by action status - required: false - schema: - $ref: '#/components/schemas/AgentActionStatus' - - name: limit + - name: accountId in: query - description: Maximum number of results to return (default 20, max 100) required: false + description: The id of the internal account whose delegated keys to list. schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor + type: string + example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + - name: fundingSourceId in: query - description: Cursor for pagination (returned from previous request) required: false + description: The id of the card funding source whose delegated keys to list. schema: type: string + example: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011 responses: '200': - description: Successful operation + description: Delegated keys matching the supplied filters. Returns an empty `data` array when no matching delegated keys are visible to the caller. content: application/json: schema: - $ref: '#/components/schemas/AgentActionListResponse' - '401': - description: Unauthorized + $ref: '#/components/schemas/DelegatedKeyListResponse' + '400': + description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Error401' + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/actions/{actionId}: - parameters: - - name: actionId - in: path - description: Unique identifier of the agent action - required: true - schema: - type: string + /auth/delegated-keys/{id}: get: - summary: Get an agent action - description: | - Retrieve a specific action submitted by the authenticated agent. Poll this endpoint after submitting an action that requires approval to check whether it has been approved, rejected, or has failed. - operationId: agentGetAction + summary: Get a delegated signing key + description: Retrieve a delegated signing key by its system-generated id. + operationId: getDelegatedKeyById tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] + parameters: + - name: id + in: path + description: The id of the delegated key to retrieve (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys` or `GET /auth/delegated-keys`). + required: true + schema: + type: string + example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/DelegatedKey' '401': description: Unauthorized content: @@ -6618,7 +6356,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Action not found + description: Delegated key not found content: application/json: schema: @@ -6629,69 +6367,42 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/transfer-in: - post: - summary: Create a transfer-in + delete: + summary: Revoke a delegated signing key description: | - Transfer funds from an external account to an internal account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy. - If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. - This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the payment instructions on the internal account to deposit funds. - operationId: agentCreateTransferIn + Revoke an `ACTIVE` delegated signing key. Grid uses the custodied delegated key to authorize deleting its own signer identity. Deleting the identity also removes its API key, after which the delegated key can no longer sign. The response is `204` when revocation completes. + + The underlying signing policies are left in place. Their consensus references the now-deleted signer identity, so they can never authorize anything, and deleting them is unnecessary for correctness or security. + operationId: revokeDelegatedKey tags: - - Agent Operations + - Embedded Wallet Auth security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + - name: id + in: path + description: The id of the delegated key to revoke (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys`). + required: true schema: type: string - example: 550e8400-e29b-41d4-a716-446655440000 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TransferInRequest' - examples: - transferIn: - summary: Transfer from external to internal account - value: - source: - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - amount: 12550 + example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021 responses: - '201': - description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. - content: - application/json: - schema: - $ref: '#/components/schemas/AgentAction' + '204': + description: Delegated key revoked. The key can no longer authorize signing. '400': - description: Bad request - Invalid parameters + description: Bad request. Returned when the delegated key has already been revoked or is not `ACTIVE`. content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Agent policy does not permit this operation or spending limit exceeded - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Account not found + description: Delegated key not found content: application/json: schema: @@ -6702,50 +6413,31 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/transfer-out: + /agents: post: - summary: Create a transfer-out + summary: Create an agent description: | - Transfer funds from an internal account to an external account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy. - If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. - operationId: agentCreateTransferOut + Create a new agent with a specified policy. Returns the created agent and a device code that must be redeemed by the agent software to complete installation. + operationId: createAgent tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] - parameters: - - name: Idempotency-Key - in: header - required: false - description: | - A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. - schema: - type: string - example: 550e8400-e29b-41d4-a716-446655440000 + - BasicAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/TransferOutRequest' - examples: - transferOut: - summary: Transfer from internal to external account - value: - source: - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - destination: - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - amount: 12550 + $ref: '#/components/schemas/AgentCreateRequest' responses: '201': - description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. + description: Agent created successfully content: application/json: schema: - $ref: '#/components/schemas/AgentAction' + $ref: '#/components/schemas/AgentCreateResponse' '400': - description: Bad request - Invalid parameters + description: Bad request content: application/json: schema: @@ -6756,47 +6448,67 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - Agent policy does not permit this operation or spending limit exceeded - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' - '404': - description: Account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/internal-accounts: get: - summary: List agent's internal accounts - description: | - Retrieve the internal accounts belonging to the customer this agent operates on behalf of. Use this to discover available source accounts for transfers and quotes, and to verify which accounts are accessible under the agent's `accountRestrictions` policy. - operationId: agentListInternalAccounts + summary: List agents + description: Retrieve a paginated list of agents for the authenticated platform. + operationId: listAgents tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: currency + - name: customerId in: query - description: Filter by currency code + description: Filter by customer ID required: false schema: type: string - - name: type + - name: isPaused 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 paused status required: false schema: - $ref: '#/components/schemas/InternalAccountType' + type: boolean + - name: isConnected + in: query + description: Filter by connection status (whether the device code has been redeemed) + required: false + schema: + type: boolean + - name: createdAfter + in: query + description: Filter agents created after this timestamp (inclusive) + required: false + schema: + type: string + format: date-time + - name: createdBefore + in: query + description: Filter agents created before this timestamp (inclusive) + required: false + schema: + type: string + format: date-time + - name: updatedAfter + in: query + description: Filter agents updated after this timestamp (inclusive) + required: false + schema: + type: string + format: date-time + - name: updatedBefore + in: query + description: Filter agents updated before this timestamp (inclusive) + required: false + schema: + type: string + format: date-time - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -6818,7 +6530,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/InternalAccountListResponse' + $ref: '#/components/schemas/AgentListResponse' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -6831,23 +6549,43 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/external-accounts: + /agents/approvals: get: - summary: List agent external accounts + summary: List agent transaction approval requests description: | - Retrieve a paginated list of external accounts belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. - operationId: agentListExternalAccounts + Retrieve a paginated list of agent actions that require platform approval. Filter by `agentId` or `customerId` to scope results to a specific agent or customer. Approve or reject individual actions via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`. + operationId: listAgentApprovals tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] + - BasicAuth: [] parameters: - - name: currency + - name: agentId in: query - description: Filter by currency code + description: Filter by agent ID + required: false + schema: + type: string + - name: customerId + in: query + description: Filter by customer ID + required: false + schema: + type: string + - name: startDate + in: query + description: Filter by start date (inclusive) in ISO 8601 format + required: false + schema: + type: string + format: date-time + - name: endDate + in: query + description: Filter by end date (inclusive) in ISO 8601 format required: false schema: type: string + format: date-time - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -6863,13 +6601,23 @@ paths: required: false schema: type: string - responses: - '200': - description: Successful operation + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc + responses: + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' + $ref: '#/components/schemas/AgentActionListResponse' '400': description: Bad request - Invalid parameters content: @@ -6888,105 +6636,58 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - post: - summary: Add an external account + /agents/me: + get: + summary: Get current agent description: | - Register a new external bank account or wallet for the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. The `customerId` field is optional and will be inferred from the agent's associated customer if omitted. - operationId: agentCreateExternalAccount + Retrieve the authenticated agent's own profile, policy, and current usage. This endpoint is called by the agent software itself using its own credentials (obtained via device code redemption) rather than platform credentials. + operationId: getAgentMe tags: - Agent Operations security: - AgentAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ExternalAccountCreateRequest' - examples: - usBankAccount: - summary: Create external US bank account - value: - currency: USD - accountInfo: - accountType: USD_ACCOUNT - accountNumber: '12345678901' - routingNumber: '123456789' - bankAccountType: CHECKING - bankName: Chase Bank - beneficiary: - beneficiaryType: INDIVIDUAL - fullName: John Doe - birthDate: '1990-01-15' - nationality: US - address: - line1: 123 Main Street - city: San Francisco - state: CA - postalCode: '94105' - country: US - sparkWallet: - summary: Create external Spark wallet - value: - currency: BTC - accountInfo: - accountType: SPARK_WALLET - address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu responses: - '201': - description: External account created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ExternalAccount' - '400': - description: Bad request + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/Agent' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - External account already exists - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /agents/me/external-accounts/{externalAccountId}: + /agents/{agentId}: parameters: - - name: externalAccountId + - name: agentId in: path - description: System-generated unique external account identifier + description: System-generated unique agent identifier required: true schema: type: string get: - summary: Get agent external account by ID - description: | - Retrieve an external account belonging to the authenticated agent's customer. Returns 404 if the account exists but belongs to a different customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. - operationId: agentGetExternalAccount + summary: Get agent by ID + description: Retrieve an agent by its system-generated ID. + operationId: getAgentById tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] + - BasicAuth: [] responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/Agent' '401': description: Unauthorized content: @@ -6994,7 +6695,52 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Agent not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + patch: + summary: Update agent + description: Update an agent's name or paused state. + operationId: updateAgent + tags: + - Agent Management + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AgentUpdateRequest' + responses: + '200': + description: Agent updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Agent' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Agent not found content: application/json: schema: @@ -7006,17 +6752,16 @@ paths: schema: $ref: '#/components/schemas/Error500' delete: - summary: Delete agent external account - description: | - Delete an external account belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. - operationId: agentDeleteExternalAccount + summary: Delete agent + description: Permanently delete an agent. Connected agent software will lose access immediately. + operationId: deleteAgent tags: - - Agent Operations + - Agent Management security: - - AgentAuth: [] + - BasicAuth: [] responses: '204': - description: External account deleted successfully + description: Agent deleted successfully '401': description: Unauthorized content: @@ -7024,7 +6769,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Agent not found content: application/json: schema: @@ -7035,18 +6780,21 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /cards: - post: - summary: Issue a card + /agents/{agentId}/policy: + parameters: + - name: agentId + in: path + description: System-generated unique agent identifier + required: true + schema: + type: string + patch: + summary: Update agent policy description: | - Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`. - - If any funding source is an Embedded Wallet internal account, the cardholder must authorize Grid to sign Spark token transactions for that card funding source by completing the delegated-key creation flow with `POST /auth/delegated-keys`. Until an active delegated key exists for that funding source, Authorization Decisioning cannot use it to fund card transactions. - - New cards start in `state: "PROCESSING"` while the card issuer provisions the card. The `card.state_change` webhook fires on each state transition, including the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails). - operationId: createCard + Partially update an agent's policy. Only provided fields will be updated; omitted fields retain their current values. Policy changes take effect immediately. + operationId: updateAgentPolicy tags: - - Cards + - Agent Management security: - BasicAuth: [] requestBody: @@ -7054,25 +6802,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CardCreateRequest' - examples: - virtualCard: - summary: Issue a virtual card with one funding source - value: - cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCardId: card-emp-aary-001 - form: VIRTUAL - fundingSources: - - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + $ref: '#/components/schemas/AgentPolicyUpdateRequest' responses: - '201': - description: Card created successfully. Newly-created cards start in `PROCESSING` while the issuer provisions them. Cards funded by an Embedded Wallet internal account also require an active delegated key for that funding source before Authorization Decisioning can use it. + '200': + description: Agent policy updated successfully content: application/json: schema: - $ref: '#/components/schemas/Card' + $ref: '#/components/schemas/Agent' '400': - description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters. + description: Bad request content: application/json: schema: @@ -7083,86 +6822,44 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error + '404': + description: Agent not found content: application/json: schema: - $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Card issuance is not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error501' - get: - summary: List cards + $ref: '#/components/schemas/Error500' + /agents/{agentId}/device-codes: + parameters: + - name: agentId + in: path + description: System-generated unique agent identifier + required: true + schema: + type: string + post: + summary: Regenerate a device code description: | - Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller. - operationId: listCards + Generate a new device code for an existing agent. Use this when the original device code has expired before being redeemed, or when the agent software needs to be reinstalled. Any previously issued unredeemed device codes for this agent are invalidated. + operationId: regenerateAgentDeviceCode tags: - - Cards + - Agent Management security: - BasicAuth: [] - parameters: - - name: cardholderId - in: query - description: Filter by cardholder (customer) id. - required: false - schema: - type: string - - name: accountId - in: query - description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id. - required: false - schema: - type: string - - name: platformCardId - in: query - description: Filter by platform-specific card identifier. - required: false - schema: - type: string - - name: state - in: query - description: Filter by card state. - required: false - schema: - $ref: '#/components/schemas/CardState' - - name: limit - in: query - description: Maximum number of results to return (default 20, max 100) - required: false - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: cursor - in: query - description: Cursor for pagination (returned from previous request) - required: false - schema: - type: string - - name: sortOrder - in: query - description: Order to sort results in - required: false - schema: - type: string - enum: - - asc - - desc - default: desc responses: - '200': - description: Successful operation + '201': + description: New device code generated successfully content: application/json: schema: - $ref: '#/components/schemas/CardListResponse' + $ref: '#/components/schemas/AgentDeviceCode' '400': - description: Bad request - Invalid parameters + description: Bad request content: application/json: schema: @@ -7173,41 +6870,62 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '500': - description: Internal service error + '404': + description: Agent not found content: application/json: schema: - $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + $ref: '#/components/schemas/Error404' + '409': + description: Conflict - Agent already has an active connection and cannot regenerate a device code content: application/json: schema: - $ref: '#/components/schemas/Error501' - /cards/{id}: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /agents/{agentId}/actions/{actionId}/approve: parameters: - - name: id + - name: agentId in: path - description: System-generated unique card identifier + description: System-generated unique agent identifier required: true schema: type: string - get: - summary: Get a card - description: Retrieve a card by its system-generated id. To display the card's full PAN, CVV, and expiry to the cardholder, request a reveal with `POST /cards/{id}/reveal` — the card resource itself never carries the reveal URL. - operationId: getCardById + - name: actionId + in: path + description: Unique identifier of the agent action to approve + required: true + schema: + type: string + post: + summary: Approve an agent action + description: | + Approve a pending agent action, allowing Grid to proceed with execution. The action must have status `PENDING_APPROVAL`. Once approved, Grid executes the underlying operation (quote execution or transfer) and the action transitions to `APPROVED`. + For `EXECUTE_QUOTE` actions, note that the underlying quote may have expired between submission and approval — in that case the action will transition to `FAILED` instead. + This endpoint is called by the platform's backend using platform credentials, not by the agent itself. + operationId: approveAgentAction tags: - - Cards + - Agent Management security: - BasicAuth: [] responses: '200': - description: Successful operation + description: Action approved successfully. Returns the updated AgentAction. content: application/json: schema: - $ref: '#/components/schemas/Card' + $ref: '#/components/schemas/AgentAction' + '400': + description: Bad request - Action cannot be approved + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -7215,128 +6933,80 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Card not found + description: Agent or action not found content: application/json: schema: $ref: '#/components/schemas/Error404' - '500': - description: Internal service error + '409': + description: Conflict - Action is not pending approval or has already been processed content: application/json: schema: - $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error501' - patch: - summary: Update a card + $ref: '#/components/schemas/Error500' + /agents/{agentId}/actions/{actionId}/reject: + parameters: + - name: agentId + in: path + description: System-generated unique agent identifier + required: true + schema: + type: string + - name: actionId + in: path + description: Unique identifier of the agent action to reject + required: true + schema: + type: string + post: + summary: Reject an agent action description: | - Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied. - - - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`. - - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`. - - Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`): - - 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. - - 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`. - - Effects: - - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed. - - `state: ACTIVE`: normal authorization behavior resumes. - - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys. - - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding. - - The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated. - operationId: updateCardById + Reject a pending agent action, preventing execution. The action must have status `PENDING_APPROVAL`. Once rejected, the action transitions to `REJECTED` and the underlying operation is not executed. + This endpoint is called by the platform's backend using platform credentials, not by the agent itself. + operationId: rejectAgentAction tags: - - Cards + - Agent Management security: - BasicAuth: [] - parameters: - - name: Grid-Wallet-Signature - in: header - required: false - description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call. - schema: - type: string - example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= - - name: Request-Id - in: header - required: false - description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. - schema: - type: string - example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 requestBody: - required: true + required: false content: application/json: schema: - $ref: '#/components/schemas/CardUpdateRequest' - examples: - freeze: - summary: Freeze an active card - value: - state: FROZEN - unfreeze: - summary: Unfreeze a frozen card - value: - state: ACTIVE - updateFundingSources: - summary: Replace the card's bound funding sources - value: - fundingSources: - - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - - InternalAccount:019542f5-b3e7-1d02-0000-000000000003 - freezeAndUpdateSources: - summary: Freeze the card and replace its funding sources in one call - value: - state: FROZEN - fundingSources: - - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 - close: - summary: Permanently close the card - value: - state: CLOSED + $ref: '#/components/schemas/AgentActionRejectRequest' responses: '200': - description: Signed retry accepted. Returns the updated card. - content: - application/json: - schema: - $ref: '#/components/schemas/Card' - '202': - description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry. + description: Action rejected successfully. Returns the updated AgentAction. content: application/json: schema: - $ref: '#/components/schemas/SignedRequestChallenge' + $ref: '#/components/schemas/AgentAction' '400': - description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters. + description: Bad request - Action cannot be rejected content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '404': - description: Card not found + description: Agent or action not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.' + description: Conflict - Action is not pending approval or has already been processed content: application/json: schema: @@ -7347,54 +7017,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. - content: - application/json: - schema: - $ref: '#/components/schemas/Error501' - /cards/{id}/reveal: + /agents/device-codes/{code}/status: parameters: - - name: id + - name: code in: path - description: System-generated unique card identifier + description: The device code to check required: true schema: type: string - post: - summary: Reveal card details - description: |- - Mint a signed, short-lived URL for the card processor's iframe that displays the card's full PAN, CVV, and expiry to the cardholder. This is the only way to obtain a reveal URL — the `Card` resource never carries one. - - Request the reveal right before rendering the iframe and render the returned `panEmbedUrl` immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log the URL — it is a bearer secret for the full card details. The card data renders inside the processor's iframe and never crosses Grid's or your servers. - - Every reveal is audit-logged with the requesting actor. - operationId: revealCard + get: + summary: Get device code status + description: | + Check whether a device code has been redeemed. Use this to poll for agent installation completion after creating an agent. + operationId: getAgentDeviceCodeStatus tags: - - Cards + - Agent Management security: - BasicAuth: [] responses: '200': - description: Reveal URL minted. + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/CardRevealResponse' + $ref: '#/components/schemas/AgentDeviceCodeStatusResponse' '401': description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden. The session has no attributable actor to audit the reveal against (for example, an impersonated dashboard session). - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Card not found + description: Device code not found content: application/json: schema: @@ -7405,90 +7059,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - '501': - description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. - content: - application/json: - schema: - $ref: '#/components/schemas/Error501' - /sandbox/cards/{id}/simulate/authorization: - post: - summary: Simulate a card authorization - description: | - Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip. - - The decisioning outcome is controlled by the last three characters of `merchant.descriptor`: - - | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved | - - Production returns `404` on this path. - operationId: sandboxSimulateCardAuthorization - tags: - - Sandbox - security: - - BasicAuth: [] - parameters: - - name: id - in: path - required: true - description: The id of the card to simulate an authorization against. - schema: - type: string - example: Card:019542f5-b3e7-1d02-0000-000000000010 - requestBody: + /agents/device-codes/{code}/redeem: + parameters: + - name: code + in: path + description: The device code to redeem required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SandboxCardAuthorizationRequest' - examples: - coffeeAuth: - summary: Approved $12.50 auth at a coffee shop - value: - amount: 1250 - currency: - code: USD - merchant: - descriptor: BLUE BOTTLE COFFEE SF - mcc: '5814' - country: US - declinedInsufficientFunds: - summary: Declined — insufficient funds (descriptor suffix `002`) - value: - amount: 50000 - currency: - code: USD - merchant: - descriptor: AMAZON RETAIL US-002 - mcc: '5942' - country: US + schema: + type: string + post: + summary: Redeem device code + description: | + Redeem a device code to obtain agent credentials. This endpoint is called by the agent software during installation. On success, returns a Bearer access token that the agent uses for all subsequent API calls. The token is returned only once and must be stored securely. + This endpoint does not require platform authentication — the device code itself serves as proof of authorization. + operationId: redeemAgentDeviceCode + tags: + - Agent Management + security: [] responses: '200': - description: Simulated authorization processed. Returns the resulting card transaction. + description: Device code redeemed successfully content: application/json: schema: - $ref: '#/components/schemas/CardTransaction' + $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse' '400': - description: Bad request - Invalid parameters + description: Bad request (e.g., code already redeemed or expired) content: application/json: schema: $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token - content: - application/json: - schema: - $ref: '#/components/schemas/Error403' '404': - description: Card not found (also returned in production for this path) + description: Device code not found content: application/json: schema: @@ -7499,54 +7101,99 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/cards/{id}/simulate/clearing: - post: - summary: Simulate a card clearing + /agents/me/transactions: + get: + summary: List agent transactions description: | - Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment. - - - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth). - - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted. - - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases). - - Production returns `404` on this path. - operationId: sandboxSimulateCardClearing + Retrieve a paginated list of transactions for the authenticated agent's customer. Results are automatically scoped to the agent's associated customer — no customer filter is needed or accepted. + operationId: agentListTransactions tags: - - Sandbox + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] parameters: - - name: id - in: path - required: true - description: The id of the card the clearing applies to. + - name: accountIdentifier + in: query + description: Filter by account identifier (matches either sender or receiver) + required: false schema: type: string - example: Card:019542f5-b3e7-1d02-0000-000000000010 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/SandboxCardClearingRequest' - examples: - tipOnTopClearing: - summary: Clearing larger than auth — exercises post-hoc pull - value: - cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 - amount: 1500 - authorizationExpiry: - summary: Clearing of 0 — exercises authorization expiry - value: - cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 - amount: 0 + - name: senderAccountIdentifier + in: query + description: Filter by sender account identifier + required: false + schema: + type: string + - name: receiverAccountIdentifier + in: query + description: Filter by receiver account identifier + required: false + schema: + type: string + - name: status + in: query + description: Filter by transaction status + required: false + schema: + $ref: '#/components/schemas/TransactionStatus' + - name: type + in: query + description: Filter by transaction type + required: false + schema: + $ref: '#/components/schemas/TransactionType' + - name: reference + in: query + description: Filter by reference + required: false + schema: + type: string + - name: startDate + in: query + description: Filter by start date (inclusive) in ISO 8601 format + required: false + schema: + type: string + format: date-time + - name: endDate + in: query + description: Filter by end date (inclusive) in ISO 8601 format + required: false + schema: + type: string + format: date-time + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc responses: '200': - description: Simulated clearing processed. Returns the updated card transaction. + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/CardTransaction' + $ref: '#/components/schemas/TransactionListResponse' '400': description: Bad request - Invalid parameters content: @@ -7559,14 +7206,44 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '403': - description: Forbidden - request was made with a production platform token + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error403' + $ref: '#/components/schemas/Error500' + /agents/me/transactions/{transactionId}: + parameters: + - name: transactionId + in: path + description: Unique identifier of the transaction + required: true + schema: + type: string + get: + summary: Get agent transaction by ID + description: | + Retrieve a specific transaction belonging to the authenticated agent's customer. Returns 404 if the transaction exists but belongs to a different customer. + operationId: agentGetTransaction + tags: + - Agent Operations + security: + - AgentAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionOneOf' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' '404': - description: Card or card transaction not found + description: Transaction not found content: application/json: schema: @@ -7577,47 +7254,41 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /sandbox/cards/{id}/simulate/return: + /agents/me/quotes: post: - summary: Simulate a card return + summary: Create a transfer quote description: | - Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund). - - Production returns `404` on this path. - operationId: sandboxSimulateCardReturn + Generate a quote for a cross-currency transfer on behalf of the authenticated agent's customer. Accounts referenced in the request must belong to the agent's customer. Requires the CREATE_QUOTES permission in the agent's policy. + If the agent's defaultExecutionMode is APPROVAL_REQUIRED, or the quote amount exceeds the agent's approvalThresholds, the resulting transaction will require explicit approval before funds move. + operationId: agentCreateQuote tags: - - Sandbox + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] parameters: - - name: id - in: path - required: true - description: The id of the card the return applies to. - schema: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: type: string - example: Card:019542f5-b3e7-1d02-0000-000000000010 + example: requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/SandboxCardReturnRequest' - examples: - fullRefund: - summary: Full refund of a $15.00 settled transaction - value: - cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 - amount: 1500 + $ref: '#/components/schemas/QuoteRequest' responses: - '200': - description: Simulated return processed. Returns the updated card transaction. + '201': + description: Transfer quote created successfully content: application/json: schema: - $ref: '#/components/schemas/CardTransaction' + $ref: '#/components/schemas/Quote' '400': - description: Bad request - Invalid parameters + description: Bad request - Missing or invalid parameters content: application/json: schema: @@ -7629,13 +7300,61 @@ paths: schema: $ref: '#/components/schemas/Error401' '403': - description: Forbidden - request was made with a production platform token + description: Forbidden - Agent policy does not permit this operation content: application/json: schema: $ref: '#/components/schemas/Error403' + '412': + description: Counterparty doesn't support UMA version + content: + application/json: + schema: + $ref: '#/components/schemas/Error412' + '424': + description: Counterparty issue + content: + application/json: + schema: + $ref: '#/components/schemas/Error424' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /agents/me/quotes/{quoteId}: + parameters: + - name: quoteId + in: path + description: ID of the quote to retrieve + required: true + schema: + type: string + get: + summary: Get agent quote by ID + description: | + Retrieve a quote created by the authenticated agent. Returns 404 if the quote exists but was not created by this agent. + operationId: agentGetQuote + tags: + - Agent Operations + security: + - AgentAuth: [] + responses: + '200': + description: Quote retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Quote' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' '404': - description: Card or card transaction not found + description: Quote not found content: application/json: schema: @@ -7646,51 +7365,75 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /stablecoin-provider-accounts: + /agents/me/quotes/{quoteId}/execute: + parameters: + - name: quoteId + in: path + required: true + description: The unique identifier of the quote to execute + schema: + type: string + example: Quote:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Link a stablecoin provider account + summary: Execute a quote description: | - Link provider API credentials for the authenticated platform. Provider credentials are account-scoped and can be reused for multiple stablecoins. Currently, the only supported provider is `BRALE`; the provider environment is derived from the authenticated Grid platform mode. - operationId: linkStablecoinProviderAccount + Execute a quote created by the authenticated agent. Requires the EXECUTE_QUOTES permission in the agent's policy. + If the agent's policy requires approval for this amount (based on execution mode or approval thresholds), the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. + Once executed, the quote cannot be cancelled. + operationId: agentExecuteQuote tags: - - Stablecoins + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] parameters: - name: Idempotency-Key in: header - description: Idempotency key for retrying this request safely. Replays return the prior result; conflicting payloads are rejected. - required: true + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. schema: type: string - maxLength: 255 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/StablecoinProviderAccountLinkRequest' + example: + - name: Grid-Wallet-Signature + in: header + required: false + description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types. + schema: + type: string + example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9 responses: - '201': - description: Stablecoin provider account linked + '200': + description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.' content: application/json: schema: - $ref: '#/components/schemas/StablecoinProviderAccount' + $ref: '#/components/schemas/AgentAction' '400': - description: Bad request + description: Bad request - Quote cannot be executed content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized + description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`. content: application/json: schema: $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - Agent policy does not permit this operation or spending limit exceeded + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Quote not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' '409': - description: Conflict + description: Conflict - Quote already executed, expired, or in invalid state content: application/json: schema: @@ -7701,25 +7444,23 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /agents/me/actions: get: - summary: List stablecoin provider account links - description: Retrieve stablecoin provider account links for the authenticated platform. - operationId: listStablecoinProviderAccounts + summary: List agent's own actions + description: | + Retrieve a paginated list of actions submitted by the authenticated agent. Use this to poll for approval decisions after submitting an action that requires approval. + operationId: agentListActions tags: - - Stablecoins + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] parameters: - - name: provider - in: query - required: false - schema: - $ref: '#/components/schemas/StablecoinProvider' - name: status in: query + description: Filter by action status required: false schema: - $ref: '#/components/schemas/StablecoinProviderAccountStatus' + $ref: '#/components/schemas/AgentActionStatus' - name: limit in: query description: Maximum number of results to return (default 20, max 100) @@ -7741,13 +7482,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/StablecoinProviderAccountListResponse' - '400': - description: Bad request - Invalid parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/AgentActionListResponse' '401': description: Unauthorized content: @@ -7760,29 +7495,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /stablecoin-provider-accounts/{stablecoinProviderAccountId}: + /agents/me/actions/{actionId}: parameters: - - name: stablecoinProviderAccountId + - name: actionId in: path - description: System-generated stablecoin provider account link identifier + description: Unique identifier of the agent action required: true schema: type: string get: - summary: Get a stablecoin provider account link - description: Retrieve a stablecoin provider account link by id for the authenticated platform. - operationId: getStablecoinProviderAccount + summary: Get an agent action + description: | + Retrieve a specific action submitted by the authenticated agent. Poll this endpoint after submitting an action that requires approval to check whether it has been approved, rejected, or has failed. + operationId: agentGetAction tags: - - Stablecoins + - Agent Operations security: - - BasicAuth: [] + - AgentAuth: [] responses: '200': description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/StablecoinProviderAccount' + $ref: '#/components/schemas/AgentAction' '401': description: Unauthorized content: @@ -7790,7 +7526,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Stablecoin provider account link not found + description: Action not found content: application/json: schema: @@ -7801,580 +7537,316 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' -webhooks: - agent-action: + /agents/me/transfer-in: post: - summary: Agent action pending approval webhook + summary: Create a transfer-in description: | - Fired when an agent submits an action that requires platform approval before Grid will execute it. Use this to send a push notification to the customer so they can review and approve or reject the action in your app. - 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. - - The payload contains the full `AgentAction` — including the embedded quote or transfer details — so you can render the approval UI without a second API call. Approve or reject via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`. - operationId: agentActionWebhook + Transfer funds from an external account to an internal account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy. + If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. + This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the payment instructions on the internal account to deposit funds. + operationId: agentCreateTransferIn tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] + - AgentAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/AgentActionWebhook' + $ref: '#/components/schemas/TransferInRequest' examples: - pendingApproval: - summary: Agent action pending approval + transferIn: + summary: Transfer from external to internal account value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000020 - type: AGENT_ACTION.PENDING_APPROVAL - timestamp: '2025-10-03T15:00:00Z' - data: - id: AgentAction:019542f5-b3e7-1d02-0000-000000000099 - agentId: Agent:019542f5-b3e7-1d02-0000-000000000042 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000010 - platformCustomerId: user-a1b2c3 - status: PENDING_APPROVAL - type: EXECUTE_QUOTE - quote: - id: Quote:019542f5-b3e7-1d02-0000-000000000006 - status: PENDING - expiresAt: '2025-10-03T15:00:30Z' - createdAt: '2025-10-03T15:00:00Z' - source: - sourceType: ACCOUNT - accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - sendingCurrency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - receivingCurrency: - code: INR - name: Indian Rupee - symbol: ₹ - decimals: 2 - totalSendingAmount: 50000 - totalReceivingAmount: 4625000 - exchangeRate: 92.5 - feesIncluded: 250 - transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000099 - createdAt: '2025-10-03T15:00:00Z' - updatedAt: '2025-10-03T15:00:00Z' + source: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + amount: 12550 responses: - '200': - description: Webhook received and acknowledged. + '201': + description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. + content: + application/json: + schema: + $ref: '#/components/schemas/AgentAction' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + '403': + description: Forbidden - Agent policy does not permit this operation or spending limit exceeded content: application/json: schema: - $ref: '#/components/schemas/Error409' - incoming-payment: + $ref: '#/components/schemas/Error403' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /agents/me/transfer-out: post: - summary: Incoming payment webhook and approval mechanism + summary: Create a transfer-out description: | - Webhook that is called when an incoming payment is received by a customer's UMA address. - 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. - - ### Payment Approval Flow - When a transaction has `status: "PENDING"`, this webhook serves as an approval mechanism: - - 1. The client should check the `counterpartyInformation` against their requirements - 2. To APPROVE the payment synchronously, return a 200 OK response - 3. To REJECT the payment, return a 403 Forbidden response with an Error object - 4. To request more information, return a 422 Unprocessable Entity with specific missing fields - 5. To process the payment asynchronously, return a 202 Accepted response and then call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint within 5 seconds. Note that synchronous approval/rejection is preferred where possible. - - The Grid system will proceed or cancel the payment based on your response. - - For transactions with other statuses (COMPLETED, FAILED, REFUNDED), this webhook is purely informational. - operationId: incomingPaymentWebhook + Transfer funds from an internal account to an external account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy. + If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`. + operationId: agentCreateTransferOut tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] + - AgentAuth: [] + parameters: + - name: Idempotency-Key + in: header + required: false + description: | + A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request. + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/IncomingPaymentWebhook' + $ref: '#/components/schemas/TransferOutRequest' examples: - pendingPayment: - summary: Pending payment example requiring approval - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT.PENDING - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000005 - status: PENDING - type: INCOMING - direction: CREDIT - destination: - destinationType: UMA_ADDRESS - umaAddress: $recipient@uma.domain - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - senderUmaAddress: $sender@external.domain - receiverUmaAddress: $recipient@uma.domain - receivedAmount: - amount: 50000 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - counterpartyInformation: - FULL_NAME: John Sender - BIRTH_DATE: '1985-06-15' - NATIONALITY: US - reconciliationInstructions: - reference: REF-123456789 - requestedReceiverCustomerInfoFields: - - name: NATIONALITY - mandatory: true - - name: POSTAL_ADDRESS - mandatory: false - incomingCompletedPayment: - summary: Completed payment notification - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: INCOMING_PAYMENT.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000005 - status: COMPLETED - type: INCOMING - direction: CREDIT - destination: - destinationType: UMA_ADDRESS - umaAddress: $recipient@uma.domain - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - senderUmaAddress: $sender@external.domain - receiverUmaAddress: $recipient@uma.domain - receivedAmount: - amount: 50000 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - settledAt: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: Payment for services - reconciliationInstructions: - reference: REF-123456789 - incomingCompletedCryptoPayment: - summary: Completed payment funded from an external crypto wallet + transferOut: + summary: Transfer from internal to external account value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000009 - type: INCOMING_PAYMENT.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000006 - status: COMPLETED - type: INCOMING - direction: CREDIT - source: - sourceType: REALTIME_FUNDING - currency: USDC - onChainTransaction: - transactionHash: 7RJWhvQBQPEjJmki5fhBboGBWRJhmcFkMvrr4Fu3tMSJ5EdynMEiYSyiWAH9GpcbHpeUzeSQF9ZY6q4x8AhBskUf - network: SOLANA - destination: - destinationType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - receivedAmount: - amount: 100000 - currency: - code: USDC - name: USD Coin - symbol: '' - decimals: 6 - settledAt: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: USDC deposit from self-custody wallet + source: + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + destination: + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + amount: 12550 responses: - '200': - description: | - Webhook received successfully. - For PENDING transactions, this indicates approval to proceed with the payment. - If `requestedReceiverCustomerInfoFields` were present in the webhook request, the corresponding fields for the recipient must be included in this response in the `receiverCustomerInfo` object. + '201': + description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. content: application/json: schema: - $ref: '#/components/schemas/IncomingPaymentWebhookResponse' - '202': - description: | - Webhook received and will be processed asynchronously. The synchronous 200 response should be preferred where possible. This asycnhronous path should only be used in - cases where the platform's architecture requires async (but still very quick) processing before approving or rejecting the payment. - The platform must call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint to approve or reject the payment within 5 seconds or the payment will be automatically rejected. + $ref: '#/components/schemas/AgentAction' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '403': - description: | - Forbidden - Payment rejected by the client. - Only applicable for PENDING transactions. + description: Forbidden - Agent policy does not permit this operation or spending limit exceeded content: application/json: schema: - $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + $ref: '#/components/schemas/Error403' + '404': + description: Account not found content: application/json: schema: - $ref: '#/components/schemas/Error409' - '422': - description: | - Unprocessable Entity - Additional counterparty information required. - Only applicable for PENDING transactions. + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/IncomingPaymentWebhookUnprocessableResponse' - outgoing-payment: - post: - summary: Outgoing payment status webhook + $ref: '#/components/schemas/Error500' + /agents/me/internal-accounts: + get: + summary: List agent's internal accounts description: | - Webhook that is called when an outgoing payment's status changes. - 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. - - This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded. - operationId: outgoingPaymentWebhook + Retrieve the internal accounts belonging to the customer this agent operates on behalf of. Use this to discover available source accounts for transfers and quotes, and to verify which accounts are accessible under the agent's `accountRestrictions` policy. + operationId: agentListInternalAccounts tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OutgoingPaymentWebhook' - examples: - outgoingCompletedPayment: - summary: Completed outgoing payment - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000005 - status: COMPLETED - type: OUTGOING - direction: DEBIT - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - senderUmaAddress: $sender@uma.domain - receiverUmaAddress: $recipient@external.domain - sentAmount: - amount: 10550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - receivedAmount: - amount: 9706 - currency: - code: EUR - name: Euro - symbol: € - decimals: 2 - exchangeRate: 0.92 - quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - settledAt: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: 'Payment for invoice #1234' - paymentInstructions: [] - rateDetails: - counterpartyMultiplier: 1.08 - counterpartyFixedFee: 10 - gridApiMultiplier: 0.925 - gridApiFixedFee: 10 - gridApiVariableFeeRate: 0.003 - gridApiVariableFeeAmount: 30 - outgoingCompletedCryptoPayment: - summary: Completed crypto payout to an external wallet - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: OUTGOING_PAYMENT.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000009 - status: COMPLETED - type: OUTGOING - direction: DEBIT - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:c34dcbd6-dced-4ec4-b756-3c3a9ea3d789 - onChainTransaction: - transactionHash: h82pJGF9p7kpzb6eU326EFZf2cDnimbTFVeJtx1qtBmUNJAEqN76R7PwPfHt3oWb8R6cKvhgyxQdDn53jFrK6wFx - network: SOLANA - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - sentAmount: - amount: 100000 - currency: - code: USDC - name: USD Coin - symbol: '' - decimals: 6 - receivedAmount: - amount: 100000 - currency: - code: USDC - name: USD Coin - symbol: '' - decimals: 6 - quoteId: Quote:019542f5-b3e7-1d02-0000-000000000010 - settledAt: '2025-08-15T14:30:00Z' - createdAt: '2025-08-15T14:25:18Z' - description: USDC withdrawal to self-custody wallet - paymentInstructions: [] - failedPayment: - summary: Failed outgoing payment - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000007 - type: OUTGOING_PAYMENT.FAILED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Transaction:019542f5-b3e7-1d02-0000-000000000005 - status: FAILED - type: OUTGOING - direction: DEBIT - source: - sourceType: ACCOUNT - accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - destination: - destinationType: ACCOUNT - accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 - customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 - platformCustomerId: 18d3e5f7b4a9c2 - senderUmaAddress: $sender@uma.domain - receiverUmaAddress: $recipient@external.domain - sentAmount: - amount: 10550 - currency: - code: USD - name: United States Dollar - symbol: $ - decimals: 2 - createdAt: '2025-08-15T14:25:18Z' - quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 - failureReason: QUOTE_EXECUTION_FAILED + - AgentAuth: [] + parameters: + - name: currency + in: query + description: Filter by currency code + required: false + schema: + 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. + required: false + schema: + $ref: '#/components/schemas/InternalAccountType' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string responses: '200': - description: Webhook received successfully - '400': - description: Bad request + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/InternalAccountListResponse' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error409' - test-webhook: - post: - summary: Test webhook for integration verification + $ref: '#/components/schemas/Error500' + /agents/me/external-accounts: + get: + summary: List agent external accounts description: | - Webhook that is sent once to verify your webhook endpoint is correctly set up. - This is sent when you configure or update your platform settings with a webhook URL. - - ### Authentication - The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by the Grid API. - 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. - - This webhook is purely for testing your endpoint integration and signature verification. - operationId: testWebhook + Retrieve a paginated list of external accounts belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. + operationId: agentListExternalAccounts tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/TestWebhookRequest' - examples: - testWebhook: - summary: Test webhook example - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000001 - type: TEST - timestamp: '2025-08-15T14:32:00Z' - data: {} + - AgentAuth: [] + parameters: + - name: currency + in: query + description: Filter by currency code + required: false + schema: + type: string + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string responses: '200': - description: Webhook received successfully. This confirms your webhook endpoint is properly configured. + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccountListResponse' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error409' - bulk-upload: + $ref: '#/components/schemas/Error500' post: - summary: Bulk upload status webhook + summary: Add an external account description: | - Webhook that is called when a bulk customer upload job completes or fails. - 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. - - This webhook is sent when a bulk upload job completes or fails, providing detailed information about the results. - operationId: bulkUploadWebhook + Register a new external bank account or wallet for the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. The `customerId` field is optional and will be inferred from the agent's associated customer if omitted. + operationId: agentCreateExternalAccount tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] + - AgentAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/BulkUploadWebhook' + $ref: '#/components/schemas/ExternalAccountCreateRequest' examples: - completedUpload: - summary: Successful bulk upload completion + usBankAccount: + summary: Create external US bank account value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD.COMPLETED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Job:019542f5-b3e7-1d02-0000-000000000006 - status: COMPLETED - progress: - total: 5000 - processed: 5000 - successful: 5000 - failed: 0 - errors: [] - failedUpload: - summary: Failed bulk upload + currency: USD + accountInfo: + accountType: USD_ACCOUNT + accountNumber: '12345678901' + routingNumber: '123456789' + bankAccountType: CHECKING + bankName: Chase Bank + beneficiary: + beneficiaryType: INDIVIDUAL + fullName: John Doe + birthDate: '1990-01-15' + nationality: US + address: + line1: 123 Main Street + city: San Francisco + state: CA + postalCode: '94105' + country: US + sparkWallet: + summary: Create external Spark wallet value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: BULK_UPLOAD.FAILED - timestamp: '2025-08-15T14:32:00Z' - data: - id: Job:019542f5-b3e7-1d02-0000-000000000006 - status: FAILED - progress: - total: 5000 - processed: 5000 - successful: 0 - failed: 5000 - errors: - - correlationId: row_1 - error: - code: invalid_csv_format - message: Invalid CSV format - details: - reason: missing_required_column - column: umaAddress + currency: BTC + accountInfo: + accountType: SPARK_WALLET + address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu responses: - '200': - description: Webhook received successfully + '201': + description: External account created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' '400': description: Bad request content: @@ -8382,121 +7854,1557 @@ webhooks: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' '409': - description: Conflict - Webhook has already been processed (duplicate id) + description: Conflict - External account already exists content: application/json: schema: $ref: '#/components/schemas/Error409' - invitation-claimed: - post: - summary: Invitation claimed webhook + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /agents/me/external-accounts/{externalAccountId}: + parameters: + - name: externalAccountId + in: path + description: System-generated unique external account identifier + required: true + schema: + type: string + get: + summary: Get agent external account by ID description: | - Webhook that is called when an invitation is claimed by a customer. - This endpoint should be implemented by platform clients of the Grid API. - - When a customer claims an invitation, this webhook is triggered to notify the platform that: - 1. The invitation has been successfully claimed - 2. The invitee UMA address is now associated with the invitation - 3. The invitation status has changed from PENDING to CLAIMED - - This allows platforms to: - - Track invitation usage and conversion rates - - Trigger onboarding flows for new customers who joined via invitation - - Apply referral bonuses or rewards to the inviter - - Update their UI to reflect the claimed status - - ### 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. - operationId: invitationClaimedWebhook + Retrieve an external account belonging to the authenticated agent's customer. Returns 404 if the account exists but belongs to a different customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. + operationId: agentGetExternalAccount tags: - - Webhooks + - Agent Operations security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/InvitationClaimedWebhook' - examples: - claimedInvitation: - summary: Invitation claimed notification - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000008 - type: INVITATION.CLAIMED - timestamp: '2025-09-01T15:45:00Z' - data: - code: 019542f5 - createdAt: '2025-09-01T14:30:00Z' - claimedAt: '2025-09-01T15:45:00Z' - inviterUma: $inviter@uma.domain - inviteeUma: $invitee@uma.domain - status: CLAIMED - url: https://uma.me/i/019542f5 + - AgentAuth: [] responses: '200': - description: Webhook received successfully - '400': - description: Bad request + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error400' + $ref: '#/components/schemas/ExternalAccount' '401': - description: Unauthorized - Signature validation failed + description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error401' - '409': - description: Conflict - Webhook has already been processed (duplicate id) + '404': + description: External account not found content: application/json: schema: - $ref: '#/components/schemas/Error409' - customer-update: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + delete: + summary: Delete agent external account + description: | + Delete an external account belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. + operationId: agentDeleteExternalAccount + tags: + - Agent Operations + security: + - AgentAuth: [] + responses: + '204': + description: External account deleted successfully + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: External account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /cards: post: - summary: Customer status change + summary: Issue a card description: | - Webhook that is called when the status of a customer is updated, including KYC and KYB status changes. - This endpoint should be implemented by clients of the Grid API. + Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`. - ### 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 API 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 any funding source is an Embedded Wallet internal account, the cardholder must authorize Grid to sign Spark token transactions for that card funding source by completing the delegated-key creation flow with `POST /auth/delegated-keys`. Until an active delegated key exists for that funding source, Authorization Decisioning cannot use it to fund card transactions. - If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. - operationId: customerStatusWebhook + New cards start in `state: "PROCESSING"` while the card issuer provisions the card. The `card.state_change` webhook fires on each state transition, including the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails). + operationId: createCard tags: - - Webhooks + - Cards security: - - WebhookSignature: [] + - BasicAuth: [] requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/CustomerWebhook' + $ref: '#/components/schemas/CardCreateRequest' examples: - kycApprovedWebhook: - summary: When an individual customer KYC has been approved + virtualCard: + summary: Issue a virtual card with one funding source + value: + cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCardId: card-emp-aary-001 + form: VIRTUAL + fundingSources: + - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + responses: + '201': + description: Card created successfully. Newly-created cards start in `PROCESSING` while the issuer provisions them. Cards funded by an Embedded Wallet internal account also require an active delegated key for that funding source before Authorization Decisioning can use it. + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + '400': + description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Card issuance is not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + get: + summary: List cards + description: | + Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller. + operationId: listCards + tags: + - Cards + security: + - BasicAuth: [] + parameters: + - name: cardholderId + in: query + description: Filter by cardholder (customer) id. + required: false + schema: + type: string + - name: accountId + in: query + description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id. + required: false + schema: + type: string + - name: platformCardId + in: query + description: Filter by platform-specific card identifier. + required: false + schema: + type: string + - name: state + in: query + description: Filter by card state. + required: false + schema: + $ref: '#/components/schemas/CardState' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string + - name: sortOrder + in: query + description: Order to sort results in + required: false + schema: + type: string + enum: + - asc + - desc + default: desc + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/CardListResponse' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /cards/{id}: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + get: + summary: Get a card + description: Retrieve a card by its system-generated id. To display the card's full PAN, CVV, and expiry to the cardholder, request a reveal with `POST /cards/{id}/reveal` — the card resource itself never carries the reveal URL. + operationId: getCardById + tags: + - Cards + security: + - BasicAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + patch: + summary: Update a card + description: | + Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied. + + - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`. + - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`. + + Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`): + + 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`. + + 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`. + + Effects: + - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed. + - `state: ACTIVE`: normal authorization behavior resumes. + - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys. + - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding. + + The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated. + operationId: updateCardById + tags: + - Cards + security: + - BasicAuth: [] + parameters: + - name: Grid-Wallet-Signature + in: header + required: false + description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call. + schema: + type: string + example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE= + - name: Request-Id + in: header + required: false + description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`. + schema: + type: string + example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CardUpdateRequest' + examples: + freeze: + summary: Freeze an active card + value: + state: FROZEN + unfreeze: + summary: Unfreeze a frozen card + value: + state: ACTIVE + updateFundingSources: + summary: Replace the card's bound funding sources + value: + fundingSources: + - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + - InternalAccount:019542f5-b3e7-1d02-0000-000000000003 + freezeAndUpdateSources: + summary: Freeze the card and replace its funding sources in one call + value: + state: FROZEN + fundingSources: + - InternalAccount:019542f5-b3e7-1d02-0000-000000000002 + close: + summary: Permanently close the card + value: + state: CLOSED + responses: + '200': + description: Signed retry accepted. Returns the updated card. + content: + application/json: + schema: + $ref: '#/components/schemas/Card' + '202': + description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry. + content: + application/json: + schema: + $ref: '#/components/schemas/SignedRequestChallenge' + '400': + description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge. + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.' + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /cards/{id}/reveal: + parameters: + - name: id + in: path + description: System-generated unique card identifier + required: true + schema: + type: string + post: + summary: Reveal card details + description: |- + Mint a signed, short-lived URL for the card processor's iframe that displays the card's full PAN, CVV, and expiry to the cardholder. This is the only way to obtain a reveal URL — the `Card` resource never carries one. + + Request the reveal right before rendering the iframe and render the returned `panEmbedUrl` immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log the URL — it is a bearer secret for the full card details. The card data renders inside the processor's iframe and never crosses Grid's or your servers. + + Every reveal is audit-logged with the requesting actor. + operationId: revealCard + tags: + - Cards + security: + - BasicAuth: [] + responses: + '200': + description: Reveal URL minted. + content: + application/json: + schema: + $ref: '#/components/schemas/CardRevealResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden. The session has no attributable actor to audit the reveal against (for example, an impersonated dashboard session). + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + '501': + description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error501' + /sandbox/cards/{id}/simulate/authorization: + post: + summary: Simulate a card authorization + description: | + Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip. + + The decisioning outcome is controlled by the last three characters of `merchant.descriptor`: + + | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved | + + Production returns `404` on this path. + operationId: sandboxSimulateCardAuthorization + tags: + - Sandbox + security: + - BasicAuth: [] + parameters: + - name: id + in: path + required: true + description: The id of the card to simulate an authorization against. + schema: + type: string + example: Card:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxCardAuthorizationRequest' + examples: + coffeeAuth: + summary: Approved $12.50 auth at a coffee shop + value: + amount: 1250 + currency: + code: USD + merchant: + descriptor: BLUE BOTTLE COFFEE SF + mcc: '5814' + country: US + declinedInsufficientFunds: + summary: Declined — insufficient funds (descriptor suffix `002`) + value: + amount: 50000 + currency: + code: USD + merchant: + descriptor: AMAZON RETAIL US-002 + mcc: '5942' + country: US + responses: + '200': + description: Simulated authorization processed. Returns the resulting card transaction. + content: + application/json: + schema: + $ref: '#/components/schemas/CardTransaction' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card not found (also returned in production for this path) + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /sandbox/cards/{id}/simulate/clearing: + post: + summary: Simulate a card clearing + description: | + Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment. + + - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth). + - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted. + - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases). + + Production returns `404` on this path. + operationId: sandboxSimulateCardClearing + tags: + - Sandbox + security: + - BasicAuth: [] + parameters: + - name: id + in: path + required: true + description: The id of the card the clearing applies to. + schema: + type: string + example: Card:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxCardClearingRequest' + examples: + tipOnTopClearing: + summary: Clearing larger than auth — exercises post-hoc pull + value: + cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 + amount: 1500 + authorizationExpiry: + summary: Clearing of 0 — exercises authorization expiry + value: + cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 + amount: 0 + responses: + '200': + description: Simulated clearing processed. Returns the updated card transaction. + content: + application/json: + schema: + $ref: '#/components/schemas/CardTransaction' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card or card transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /sandbox/cards/{id}/simulate/return: + post: + summary: Simulate a card return + description: | + Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund). + + Production returns `404` on this path. + operationId: sandboxSimulateCardReturn + tags: + - Sandbox + security: + - BasicAuth: [] + parameters: + - name: id + in: path + required: true + description: The id of the card the return applies to. + schema: + type: string + example: Card:019542f5-b3e7-1d02-0000-000000000010 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SandboxCardReturnRequest' + examples: + fullRefund: + summary: Full refund of a $15.00 settled transaction + value: + cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100 + amount: 1500 + responses: + '200': + description: Simulated return processed. Returns the updated card transaction. + content: + application/json: + schema: + $ref: '#/components/schemas/CardTransaction' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '403': + description: Forbidden - request was made with a production platform token + content: + application/json: + schema: + $ref: '#/components/schemas/Error403' + '404': + description: Card or card transaction not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /stablecoin-provider-accounts: + post: + summary: Link a stablecoin provider account + description: | + Link provider API credentials for the authenticated platform. Provider credentials are account-scoped and can be reused for multiple stablecoins. Currently, the only supported provider is `BRALE`; the provider environment is derived from the authenticated Grid platform mode. + operationId: linkStablecoinProviderAccount + tags: + - Stablecoins + security: + - BasicAuth: [] + parameters: + - name: Idempotency-Key + in: header + description: Idempotency key for retrying this request safely. Replays return the prior result; conflicting payloads are rejected. + required: true + schema: + type: string + maxLength: 255 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StablecoinProviderAccountLinkRequest' + responses: + '201': + description: Stablecoin provider account linked + content: + application/json: + schema: + $ref: '#/components/schemas/StablecoinProviderAccount' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '409': + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + get: + summary: List stablecoin provider account links + description: Retrieve stablecoin provider account links for the authenticated platform. + operationId: listStablecoinProviderAccounts + tags: + - Stablecoins + security: + - BasicAuth: [] + parameters: + - name: provider + in: query + required: false + schema: + $ref: '#/components/schemas/StablecoinProvider' + - name: status + in: query + required: false + schema: + $ref: '#/components/schemas/StablecoinProviderAccountStatus' + - name: limit + in: query + description: Maximum number of results to return (default 20, max 100) + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: cursor + in: query + description: Cursor for pagination (returned from previous request) + required: false + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/StablecoinProviderAccountListResponse' + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /stablecoin-provider-accounts/{stablecoinProviderAccountId}: + parameters: + - name: stablecoinProviderAccountId + in: path + description: System-generated stablecoin provider account link identifier + required: true + schema: + type: string + get: + summary: Get a stablecoin provider account link + description: Retrieve a stablecoin provider account link by id for the authenticated platform. + operationId: getStablecoinProviderAccount + tags: + - Stablecoins + security: + - BasicAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/StablecoinProviderAccount' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Stablecoin provider account link not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' +webhooks: + agent-action: + post: + summary: Agent action pending approval webhook + description: | + Fired when an agent submits an action that requires platform approval before Grid will execute it. Use this to send a push notification to the customer so they can review and approve or reject the action in your app. + 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. + + The payload contains the full `AgentAction` — including the embedded quote or transfer details — so you can render the approval UI without a second API call. Approve or reject via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`. + operationId: agentActionWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AgentActionWebhook' + examples: + pendingApproval: + summary: Agent action pending approval + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000020 + type: AGENT_ACTION.PENDING_APPROVAL + timestamp: '2025-10-03T15:00:00Z' + data: + id: AgentAction:019542f5-b3e7-1d02-0000-000000000099 + agentId: Agent:019542f5-b3e7-1d02-0000-000000000042 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000010 + platformCustomerId: user-a1b2c3 + status: PENDING_APPROVAL + type: EXECUTE_QUOTE + quote: + id: Quote:019542f5-b3e7-1d02-0000-000000000006 + status: PENDING + expiresAt: '2025-10-03T15:00:30Z' + createdAt: '2025-10-03T15:00:00Z' + source: + sourceType: ACCOUNT + accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + sendingCurrency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + receivingCurrency: + code: INR + name: Indian Rupee + symbol: ₹ + decimals: 2 + totalSendingAmount: 50000 + totalReceivingAmount: 4625000 + exchangeRate: 92.5 + feesIncluded: 250 + transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000099 + createdAt: '2025-10-03T15:00:00Z' + updatedAt: '2025-10-03T15:00:00Z' + responses: + '200': + description: Webhook received and acknowledged. + '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' + incoming-payment: + post: + summary: Incoming payment webhook and approval mechanism + description: | + Webhook that is called when an incoming payment is received by a customer's UMA address. + 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. + + ### Payment Approval Flow + When a transaction has `status: "PENDING"`, this webhook serves as an approval mechanism: + + 1. The client should check the `counterpartyInformation` against their requirements + 2. To APPROVE the payment synchronously, return a 200 OK response + 3. To REJECT the payment, return a 403 Forbidden response with an Error object + 4. To request more information, return a 422 Unprocessable Entity with specific missing fields + 5. To process the payment asynchronously, return a 202 Accepted response and then call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint within 5 seconds. Note that synchronous approval/rejection is preferred where possible. + + The Grid system will proceed or cancel the payment based on your response. + + For transactions with other statuses (COMPLETED, FAILED, REFUNDED), this webhook is purely informational. + operationId: incomingPaymentWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/IncomingPaymentWebhook' + examples: + pendingPayment: + summary: Pending payment example requiring approval + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.PENDING + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000005 + status: PENDING + type: INCOMING + direction: CREDIT + destination: + destinationType: UMA_ADDRESS + umaAddress: $recipient@uma.domain + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + senderUmaAddress: $sender@external.domain + receiverUmaAddress: $recipient@uma.domain + receivedAmount: + amount: 50000 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + counterpartyInformation: + FULL_NAME: John Sender + BIRTH_DATE: '1985-06-15' + NATIONALITY: US + reconciliationInstructions: + reference: REF-123456789 + requestedReceiverCustomerInfoFields: + - name: NATIONALITY + mandatory: true + - name: POSTAL_ADDRESS + mandatory: false + incomingCompletedPayment: + summary: Completed payment notification + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: INCOMING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000005 + status: COMPLETED + type: INCOMING + direction: CREDIT + destination: + destinationType: UMA_ADDRESS + umaAddress: $recipient@uma.domain + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + senderUmaAddress: $sender@external.domain + receiverUmaAddress: $recipient@uma.domain + receivedAmount: + amount: 50000 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: Payment for services + reconciliationInstructions: + reference: REF-123456789 + incomingCompletedCryptoPayment: + summary: Completed payment funded from an external crypto wallet + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000009 + type: INCOMING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000006 + status: COMPLETED + type: INCOMING + direction: CREDIT + source: + sourceType: REALTIME_FUNDING + currency: USDC + onChainTransaction: + transactionHash: 7RJWhvQBQPEjJmki5fhBboGBWRJhmcFkMvrr4Fu3tMSJ5EdynMEiYSyiWAH9GpcbHpeUzeSQF9ZY6q4x8AhBskUf + network: SOLANA + destination: + destinationType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + receivedAmount: + amount: 100000 + currency: + code: USDC + name: USD Coin + symbol: '' + decimals: 6 + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: USDC deposit from self-custody wallet + responses: + '200': + description: | + Webhook received successfully. + For PENDING transactions, this indicates approval to proceed with the payment. + If `requestedReceiverCustomerInfoFields` were present in the webhook request, the corresponding fields for the recipient must be included in this response in the `receiverCustomerInfo` object. + content: + application/json: + schema: + $ref: '#/components/schemas/IncomingPaymentWebhookResponse' + '202': + description: | + Webhook received and will be processed asynchronously. The synchronous 200 response should be preferred where possible. This asycnhronous path should only be used in + cases where the platform's architecture requires async (but still very quick) processing before approving or rejecting the payment. + The platform must call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint to approve or reject the payment within 5 seconds or the payment will be automatically rejected. + '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' + '403': + description: | + Forbidden - Payment rejected by the client. + Only applicable for PENDING transactions. + content: + application/json: + schema: + $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse' + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '422': + description: | + Unprocessable Entity - Additional counterparty information required. + Only applicable for PENDING transactions. + content: + application/json: + schema: + $ref: '#/components/schemas/IncomingPaymentWebhookUnprocessableResponse' + outgoing-payment: + post: + summary: Outgoing payment status webhook + description: | + Webhook that is called when an outgoing payment's status changes. + 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. + + This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded. + operationId: outgoingPaymentWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OutgoingPaymentWebhook' + examples: + outgoingCompletedPayment: + summary: Completed outgoing payment + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000005 + status: COMPLETED + type: OUTGOING + direction: DEBIT + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + senderUmaAddress: $sender@uma.domain + receiverUmaAddress: $recipient@external.domain + sentAmount: + amount: 10550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + receivedAmount: + amount: 9706 + currency: + code: EUR + name: Euro + symbol: € + decimals: 2 + exchangeRate: 0.92 + quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: 'Payment for invoice #1234' + paymentInstructions: [] + rateDetails: + counterpartyMultiplier: 1.08 + counterpartyFixedFee: 10 + gridApiMultiplier: 0.925 + gridApiFixedFee: 10 + gridApiVariableFeeRate: 0.003 + gridApiVariableFeeAmount: 30 + outgoingCompletedCryptoPayment: + summary: Completed crypto payout to an external wallet + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: OUTGOING_PAYMENT.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000009 + status: COMPLETED + type: OUTGOING + direction: DEBIT + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:c34dcbd6-dced-4ec4-b756-3c3a9ea3d789 + onChainTransaction: + transactionHash: h82pJGF9p7kpzb6eU326EFZf2cDnimbTFVeJtx1qtBmUNJAEqN76R7PwPfHt3oWb8R6cKvhgyxQdDn53jFrK6wFx + network: SOLANA + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + sentAmount: + amount: 100000 + currency: + code: USDC + name: USD Coin + symbol: '' + decimals: 6 + receivedAmount: + amount: 100000 + currency: + code: USDC + name: USD Coin + symbol: '' + decimals: 6 + quoteId: Quote:019542f5-b3e7-1d02-0000-000000000010 + settledAt: '2025-08-15T14:30:00Z' + createdAt: '2025-08-15T14:25:18Z' + description: USDC withdrawal to self-custody wallet + paymentInstructions: [] + failedPayment: + summary: Failed outgoing payment + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000007 + type: OUTGOING_PAYMENT.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Transaction:019542f5-b3e7-1d02-0000-000000000005 + status: FAILED + type: OUTGOING + direction: DEBIT + source: + sourceType: ACCOUNT + accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + destination: + destinationType: ACCOUNT + accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123 + customerId: Customer:019542f5-b3e7-1d02-0000-000000000001 + platformCustomerId: 18d3e5f7b4a9c2 + senderUmaAddress: $sender@uma.domain + receiverUmaAddress: $recipient@external.domain + sentAmount: + amount: 10550 + currency: + code: USD + name: United States Dollar + symbol: $ + decimals: 2 + createdAt: '2025-08-15T14:25:18Z' + quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006 + failureReason: QUOTE_EXECUTION_FAILED + 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' + test-webhook: + post: + summary: Test webhook for integration verification + description: | + Webhook that is sent once to verify your webhook endpoint is correctly set up. + This is sent when you configure or update your platform settings with a webhook URL. + + ### Authentication + The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by the Grid API. + 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. + + This webhook is purely for testing your endpoint integration and signature verification. + operationId: testWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TestWebhookRequest' + examples: + testWebhook: + summary: Test webhook example + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000001 + type: TEST + timestamp: '2025-08-15T14:32:00Z' + data: {} + responses: + '200': + description: Webhook received successfully. This confirms your webhook endpoint is properly configured. + '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' + bulk-upload: + post: + summary: Bulk upload status webhook + description: | + Webhook that is called when a bulk customer upload job completes or fails. + 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. + + This webhook is sent when a bulk upload job completes or fails, providing detailed information about the results. + operationId: bulkUploadWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BulkUploadWebhook' + examples: + completedUpload: + summary: Successful bulk upload completion + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.COMPLETED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Job:019542f5-b3e7-1d02-0000-000000000006 + status: COMPLETED + progress: + total: 5000 + processed: 5000 + successful: 5000 + failed: 0 + errors: [] + failedUpload: + summary: Failed bulk upload + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: BULK_UPLOAD.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: Job:019542f5-b3e7-1d02-0000-000000000006 + status: FAILED + progress: + total: 5000 + processed: 5000 + successful: 0 + failed: 5000 + errors: + - correlationId: row_1 + error: + code: invalid_csv_format + message: Invalid CSV format + details: + reason: missing_required_column + column: umaAddress + 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' + invitation-claimed: + post: + summary: Invitation claimed webhook + description: | + Webhook that is called when an invitation is claimed by a customer. + This endpoint should be implemented by platform clients of the Grid API. + + When a customer claims an invitation, this webhook is triggered to notify the platform that: + 1. The invitation has been successfully claimed + 2. The invitee UMA address is now associated with the invitation + 3. The invitation status has changed from PENDING to CLAIMED + + This allows platforms to: + - Track invitation usage and conversion rates + - Trigger onboarding flows for new customers who joined via invitation + - Apply referral bonuses or rewards to the inviter + - Update their UI to reflect the claimed status + + ### 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. + operationId: invitationClaimedWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/InvitationClaimedWebhook' + examples: + claimedInvitation: + summary: Invitation claimed notification + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000008 + type: INVITATION.CLAIMED + timestamp: '2025-09-01T15:45:00Z' + data: + code: 019542f5 + createdAt: '2025-09-01T14:30:00Z' + claimedAt: '2025-09-01T15:45:00Z' + inviterUma: $inviter@uma.domain + inviteeUma: $invitee@uma.domain + status: CLAIMED + url: https://uma.me/i/019542f5 + 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' + customer-update: + post: + summary: Customer status change + description: | + Webhook that is called when the status of a customer is updated, including KYC and KYB status changes. + 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 API 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. + operationId: customerStatusWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CustomerWebhook' + examples: + kycApprovedWebhook: + summary: When an individual customer KYC has been approved value: id: Webhook:019542f5-b3e7-1d02-0000-000000000007 type: CUSTOMER.KYC_APPROVED @@ -10110,7 +11018,131 @@ components: example: OPERATING_REVENUE BusinessInfoUpdate: type: object - description: Additional information for business entities + description: Additional information for business entities + properties: + legalName: + type: string + description: Legal name of the business + example: Acme Corporation, Inc. + doingBusinessAs: + type: string + description: Trade name or DBA name of the business, if different from the legal name + example: Acme + country: + type: string + description: Country of incorporation or registration (ISO 3166-1 alpha-2) + example: US + registrationNumber: + type: string + description: Business registration number + example: '5523041' + incorporatedOn: + type: string + format: date + description: Date of incorporation in ISO 8601 format (YYYY-MM-DD) + example: '2018-03-14' + entityType: + $ref: '#/components/schemas/EntityType' + taxId: + type: string + description: Tax identification number + example: 47-1234567 + countriesOfOperation: + type: array + items: + type: string + description: List of countries where the business operates (ISO 3166-1 alpha-2) + example: + - US + businessType: + $ref: '#/components/schemas/BusinessType' + purposeOfAccount: + $ref: '#/components/schemas/PurposeOfAccount' + sourceOfFunds: + type: string + description: The primary source of funds for the business + example: Funds derived from customer payments for software services + expectedMonthlyTransactionCount: + type: string + enum: + - COUNT_UNDER_10 + - COUNT_10_TO_100 + - COUNT_100_TO_500 + - COUNT_500_TO_1000 + - COUNT_OVER_1000 + description: Expected number of transactions per month + example: COUNT_100_TO_500 + expectedMonthlyTransactionVolume: + type: string + enum: + - VOLUME_UNDER_10K + - VOLUME_10K_TO_100K + - VOLUME_100K_TO_1M + - VOLUME_1M_TO_10M + - VOLUME_OVER_10M + description: Expected total transaction volume per month in USD equivalent + example: VOLUME_100K_TO_1M + expectedRecipientJurisdictions: + type: array + items: + type: string + description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2) + example: + - US + naicsCode: + type: string + pattern: ^\d{2,6}$ + description: NAICS code describing the nature of the business (2-6 digits) + example: '541511' + sourceOfFundsCategories: + type: array + items: + $ref: '#/components/schemas/SourceOfFundsCategory' + description: Structured source-of-funds categories for the business + example: + - OPERATING_REVENUE + sourceOfFundsOtherDescription: + type: string + minLength: 1 + maxLength: 500 + description: Description of the source of funds when OTHER is selected + example: Proceeds from a legal settlement + purposeOfAccountOtherDescription: + type: string + minLength: 1 + maxLength: 500 + description: Description of the account purpose when OTHER is selected + example: Escrow for equipment leases + expectedCounterpartyCountries: + type: array + items: + type: string + description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) + example: + - US + BusinessCustomerFields: + type: object + required: + - customerType + properties: + customerType: + type: string + enum: + - BUSINESS + kybStatus: + $ref: '#/components/schemas/KybStatus' + address: + $ref: '#/components/schemas/Address' + businessInfo: + $ref: '#/components/schemas/BusinessInfoUpdate' + BusinessInfoResponse: + type: object + description: | + Business information returned on a customer. `taxId` and `incorporatedOn` are + required on creation but may be absent on legacy customers that pre-date the + requirement, so both are optional in responses. + required: + - legalName properties: legalName: type: string @@ -10157,84 +11189,309 @@ components: expectedMonthlyTransactionCount: type: string enum: - - COUNT_UNDER_10 - - COUNT_10_TO_100 - - COUNT_100_TO_500 - - COUNT_500_TO_1000 - - COUNT_OVER_1000 - description: Expected number of transactions per month - example: COUNT_100_TO_500 - expectedMonthlyTransactionVolume: + - COUNT_UNDER_10 + - COUNT_10_TO_100 + - COUNT_100_TO_500 + - COUNT_500_TO_1000 + - COUNT_OVER_1000 + description: Expected number of transactions per month + example: COUNT_100_TO_500 + expectedMonthlyTransactionVolume: + type: string + enum: + - VOLUME_UNDER_10K + - VOLUME_10K_TO_100K + - VOLUME_100K_TO_1M + - VOLUME_1M_TO_10M + - VOLUME_OVER_10M + description: Expected total transaction volume per month in USD equivalent + example: VOLUME_100K_TO_1M + expectedRecipientJurisdictions: + type: array + items: + type: string + description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2) + example: + - US + naicsCode: + type: string + pattern: ^\d{2,6}$ + description: NAICS code describing the nature of the business (2-6 digits) + example: '541511' + sourceOfFundsCategories: + type: array + items: + $ref: '#/components/schemas/SourceOfFundsCategory' + description: Structured source-of-funds categories for the business + example: + - OPERATING_REVENUE + sourceOfFundsOtherDescription: + type: string + minLength: 1 + maxLength: 500 + description: Description of the source of funds when OTHER is selected + example: Proceeds from a legal settlement + purposeOfAccountOtherDescription: + type: string + minLength: 1 + maxLength: 500 + description: Description of the account purpose when OTHER is selected + example: Escrow for equipment leases + expectedCounterpartyCountries: + type: array + items: + type: string + description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) + example: + - US + BeneficialOwnerRole: + type: string + enum: + - UBO + - DIRECTOR + - COMPANY_OFFICER + - CONTROL_PERSON + - TRUSTEE + - GENERAL_PARTNER + description: Role of the beneficial owner within the business + example: UBO + IdentificationType: + type: string + enum: + - SSN + - ITIN + - EIN + - NON_US_TAX_ID + description: Type of tax identification + example: SSN + BeneficialOwnerPersonalInfo: + type: object + required: + - firstName + - lastName + - birthDate + - nationality + - address + - idType + - identifier + properties: + firstName: + type: string + description: First name of the individual + example: Jane + middleName: + type: string + description: Middle name of the individual + example: Marie + lastName: + type: string + description: Last name of the individual + example: Smith + birthDate: + type: string + format: date + description: Date of birth in ISO 8601 format (YYYY-MM-DD) + example: '1978-06-15' + nationality: + type: string + description: Country of nationality (ISO 3166-1 alpha-2) + example: US + email: + type: string + format: email + description: Email address of the individual + example: jane.smith@acmecorp.com + phoneNumber: + type: string + description: Phone number in E.164 format + example: '+14155550192' + pattern: ^\+[1-9]\d{1,14}$ + address: + $ref: '#/components/schemas/Address' + idType: + $ref: '#/components/schemas/IdentificationType' + identifier: + type: string + description: The identification number or value + example: 123-45-6789 + countryOfIssuance: + type: string + description: Country that issued the identification (ISO 3166-1 alpha-2) + example: US + BeneficialOwner: + type: object + required: + - id + - customerId + - roles + - ownershipPercentage + - personalInfo + - kycStatus + - createdAt + properties: + id: + type: string + description: Unique identifier for this beneficial owner + example: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000001 + customerId: + type: string + description: The ID of the business customer this beneficial owner is associated with + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + roles: + type: array + items: + $ref: '#/components/schemas/BeneficialOwnerRole' + description: Roles of this person within the business + example: + - UBO + - DIRECTOR + ownershipPercentage: + type: integer + description: Percentage of ownership in the business (0-100) + minimum: 0 + maximum: 100 + example: 51 + personalInfo: + $ref: '#/components/schemas/BeneficialOwnerPersonalInfo' + kycStatus: + $ref: '#/components/schemas/KycStatus' + createdAt: + type: string + format: date-time + description: When this beneficial owner was created + example: '2025-10-03T12:00:00Z' + updatedAt: + type: string + format: date-time + description: When this beneficial owner was last updated + example: '2025-10-03T12:00:00Z' + BusinessCustomer: + title: Business Customer + allOf: + - $ref: '#/components/schemas/Customer' + - $ref: '#/components/schemas/BusinessCustomerFields' + - type: object + properties: + businessInfo: + $ref: '#/components/schemas/BusinessInfoResponse' + beneficialOwners: + type: array + items: + $ref: '#/components/schemas/BeneficialOwner' + CustomerOneOf: + oneOf: + - $ref: '#/components/schemas/IndividualCustomer' + - $ref: '#/components/schemas/BusinessCustomer' + discriminator: + propertyName: customerType + mapping: + INDIVIDUAL: '#/components/schemas/IndividualCustomer' + BUSINESS: '#/components/schemas/BusinessCustomer' + CustomerListResponse: + type: object + required: + - data + - hasMore + properties: + data: + type: array + description: List of customers matching the filter criteria + items: + $ref: '#/components/schemas/CustomerOneOf' + 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) + Error405: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 405 + description: HTTP status code + code: type: string + description: | + | Error Code | Description | + |------------|-------------| + | METHOD_NOT_ALLOWED | The HTTP method is not supported for this endpoint | enum: - - VOLUME_UNDER_10K - - VOLUME_10K_TO_100K - - VOLUME_100K_TO_1M - - VOLUME_1M_TO_10M - - VOLUME_OVER_10M - description: Expected total transaction volume per month in USD equivalent - example: VOLUME_100K_TO_1M - expectedRecipientJurisdictions: - type: array - items: - type: string - description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2) - example: - - US - naicsCode: + - METHOD_NOT_ALLOWED + message: type: string - pattern: ^\d{2,6}$ - description: NAICS code describing the nature of the business (2-6 digits) - example: '541511' - sourceOfFundsCategories: - type: array - items: - $ref: '#/components/schemas/SourceOfFundsCategory' - description: Structured source-of-funds categories for the business - example: - - OPERATING_REVENUE - sourceOfFundsOtherDescription: + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + CustomerCreateRequest: + type: object + required: + - customerType + properties: + platformCustomerId: type: string - minLength: 1 - maxLength: 500 - description: Description of the source of funds when OTHER is selected - example: Proceeds from a legal settlement - purposeOfAccountOtherDescription: + description: Platform-specific customer identifier. If not provided, one will be generated by the system. + example: 9f84e0c2a72c4fa + customerType: + $ref: '#/components/schemas/CustomerType' + region: type: string - minLength: 1 - maxLength: 500 - description: Description of the account purpose when OTHER is selected - example: Escrow for equipment leases - expectedCounterpartyCountries: + description: Country code (ISO 3166-1 alpha-2) representing the customer's regional identity. This determines the regulatory jurisdiction and KYC requirements for the customer. Required if the customer will use currencies with different KYC requirements across regions. A customer with accounts in multiple regions should be registered as separate customers. This field is immutable after creation. + example: US + currencies: type: array items: type: string - description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) + description: List of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Required if the customer will use more than one sending currency, since the correct currencies cannot always be inferred. If not provided, currencies will be inferred from the customer's region. Some currency combinations may require separate customers — if so, the request will be rejected with details. example: - - US - BusinessCustomerFields: - type: object - required: - - customerType - properties: - customerType: + - USD + - USDC + email: type: string - enum: - - BUSINESS - kybStatus: - $ref: '#/components/schemas/KybStatus' - address: - $ref: '#/components/schemas/Address' - businessInfo: - $ref: '#/components/schemas/BusinessInfoUpdate' - BusinessInfoResponse: + format: email + description: Email address for the customer. + example: john.doe@example.com + phoneNumber: + type: string + pattern: ^\+[1-9]\d{1,14}$ + description: Phone number for the customer in strict E.164 format. + example: '+14155551234' + umaAddress: + type: string + description: Optional UMA address identifier. If not provided during customer creation, one will be generated by the system. If provided during customer update, the UMA address will be updated to the provided value. This is an optional identifier to route payments to the customer. This is an optional identifier to route payments to the customer. + example: $john.doe@uma.domain.com + IndividualCustomerCreateRequest: + title: Individual Customer Create Request + allOf: + - $ref: '#/components/schemas/CustomerCreateRequest' + - $ref: '#/components/schemas/IndividualCustomerFields' + - type: object + properties: + idType: + $ref: '#/components/schemas/IdentificationType' + identifier: + type: string + writeOnly: true + description: The individual's tax identification number. Required to onboard the individual as a US account holder. Only SSN and ITIN are currently accepted for an individual account holder; other identification types are rejected. Write-only — never returned in customer responses. + example: 123-45-6789 + BusinessInfo: type: object - description: | - Business information returned on a customer. `taxId` and `incorporatedOn` are - required on creation but may be absent on legacy customers that pre-date the - requirement, so both are optional in responses. + description: Additional information required for business entities required: - legalName + - taxId + - incorporatedOn properties: legalName: type: string @@ -10336,454 +11593,621 @@ components: description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) example: - US - BeneficialOwnerRole: - type: string - enum: - - UBO - - DIRECTOR - - COMPANY_OFFICER - - CONTROL_PERSON - - TRUSTEE - - GENERAL_PARTNER - description: Role of the beneficial owner within the business - example: UBO - IdentificationType: - type: string - enum: - - SSN - - ITIN - - EIN - - NON_US_TAX_ID - description: Type of tax identification - example: SSN - BeneficialOwnerPersonalInfo: + BusinessCustomerCreateRequest: + title: Business Customer Create Request + allOf: + - $ref: '#/components/schemas/CustomerCreateRequest' + - $ref: '#/components/schemas/BusinessCustomerFields' + - type: object + properties: + businessInfo: + $ref: '#/components/schemas/BusinessInfo' + CustomerCreateRequestOneOf: + oneOf: + - $ref: '#/components/schemas/IndividualCustomerCreateRequest' + - $ref: '#/components/schemas/BusinessCustomerCreateRequest' + discriminator: + propertyName: customerType + mapping: + INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest' + BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest' + Error409: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 409 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL | Transaction is not pending platform approval | + | UMA_ADDRESS_EXISTS | UMA address already exists | + | EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential | + | EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued | + | PASSKEY_ALREADY_ENROLLED | The customer already has an enrolled passkey factor; only one passkey per customer is supported. Delete the existing one before enrolling another | + | CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform | + enum: + - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL + - UMA_ADDRESS_EXISTS + - EMAIL_OTP_EMAIL_ALREADY_EXISTS + - EMAIL_OTP_CREDENTIAL_SET_CHANGED + - PASSKEY_ALREADY_ENROLLED + - CONFLICT + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + Error404: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 404 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | TRANSACTION_NOT_FOUND | Transaction not found | + | INVITATION_NOT_FOUND | Invitation not found | + | USER_NOT_FOUND | Customer not found | + | QUOTE_NOT_FOUND | Quote not found | + | LOOKUP_REQUEST_NOT_FOUND | Lookup request not found | + | TOKEN_NOT_FOUND | Token not found | + | BULK_UPLOAD_JOB_NOT_FOUND | Bulk upload job not found | + | REFERENCE_NOT_FOUND | Reference not found | + | UMA_NOT_FOUND | The UMA address is well-formed but no receiver exists at the counterparty VASP | + | STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND | Stablecoin provider account link not found | + enum: + - TRANSACTION_NOT_FOUND + - INVITATION_NOT_FOUND + - USER_NOT_FOUND + - QUOTE_NOT_FOUND + - LOOKUP_REQUEST_NOT_FOUND + - TOKEN_NOT_FOUND + - BULK_UPLOAD_JOB_NOT_FOUND + - REFERENCE_NOT_FOUND + - UMA_NOT_FOUND + - STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + Error410: type: object required: - - firstName - - lastName - - birthDate - - nationality - - address - - idType - - identifier + - message + - status + - code properties: - firstName: - type: string - description: First name of the individual - example: Jane - middleName: - type: string - description: Middle name of the individual - example: Marie - lastName: - type: string - description: Last name of the individual - example: Smith - birthDate: + status: + type: integer + enum: + - 410 + description: HTTP status code + code: type: string - format: date - description: Date of birth in ISO 8601 format (YYYY-MM-DD) - example: '1978-06-15' - nationality: + description: | + | Error Code | Description | + |------------|-------------| + | CUSTOMER_DELETED | Customer has been permanently deleted | + enum: + - CUSTOMER_DELETED + message: type: string - description: Country of nationality (ISO 3166-1 alpha-2) - example: US + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + CustomerUpdateRequest: + title: Customer Update Request + description: Request body for `PATCH /customers/{customerId}`. When `email` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer email and every tied `EMAIL_OTP` credential through the endpoint's signed-retry flow. When `phoneNumber` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer phone number and every tied `SMS_OTP` credential through the same signed-retry flow. Update `email` and `phoneNumber` in separate PATCH calls. + type: object + required: + - customerType + properties: + customerType: + $ref: '#/components/schemas/CustomerType' + currencies: + type: array + items: + type: string + description: Updated list of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Replaces the existing list. Some currency combinations may require separate customers — if so, the request will be rejected with details. + example: + - USD + - EUR + - USDC email: type: string format: email - description: Email address of the individual - example: jane.smith@acmecorp.com + description: Email address for the customer. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `EMAIL_OTP` credential across all tied Embedded Wallets. + example: john.doe@example.com phoneNumber: type: string - description: Phone number in E.164 format - example: '+14155550192' pattern: ^\+[1-9]\d{1,14}$ - address: - $ref: '#/components/schemas/Address' - idType: - $ref: '#/components/schemas/IdentificationType' - identifier: - type: string - description: The identification number or value - example: 123-45-6789 - countryOfIssuance: + description: Phone number for the customer in strict E.164 format. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `SMS_OTP` credential across all tied Embedded Wallets. Send phone number and email updates as separate PATCH calls. + example: '+14155551234' + umaAddress: type: string - description: Country that issued the identification (ISO 3166-1 alpha-2) - example: US - BeneficialOwner: + description: Optional UMA address identifier. If provided, the customer's UMA address will be updated. This is an optional identifier to route payments to the customer. + example: $john.doe@uma.domain.com + IndividualCustomerUpdateRequest: + title: Individual Customer Update Request + allOf: + - $ref: '#/components/schemas/CustomerUpdateRequest' + - $ref: '#/components/schemas/IndividualCustomerFields' + BusinessCustomerUpdateRequest: + title: Business Customer Update Request + allOf: + - $ref: '#/components/schemas/CustomerUpdateRequest' + - $ref: '#/components/schemas/BusinessCustomerFields' + CustomerUpdateRequestOneOf: + oneOf: + - $ref: '#/components/schemas/IndividualCustomerUpdateRequest' + - $ref: '#/components/schemas/BusinessCustomerUpdateRequest' + discriminator: + propertyName: customerType + mapping: + INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' + BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' + SignedRequestChallenge: + title: Signed Request Challenge type: object required: - - id - - customerId - - roles - - ownershipPercentage - - personalInfo - - kycStatus - - createdAt + - payloadToSign + - requestId + - expiresAt + description: Common base for two-step signed-retry challenge responses on Embedded Wallet endpoints (credential registration or revocation, session refresh or revocation, wallet export, customer email updates, and similar). Holds the signing fields shared across every challenge shape; each variant composes this base via `allOf` and adds its own resource `id` (and `type`, when applicable) with variant-specific description and example. properties: - id: + payloadToSign: type: string - description: Unique identifier for this beneficial owner - example: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000001 - customerId: + description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request. + example: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_EXAMPLE"}' + requestId: type: string - description: The ID of the business customer this beneficial owner is associated with - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - roles: - type: array - items: - $ref: '#/components/schemas/BeneficialOwnerRole' - description: Roles of this person within the business - example: - - UBO - - DIRECTOR - ownershipPercentage: - type: integer - description: Percentage of ownership in the business (0-100) - minimum: 0 - maximum: 100 - example: 51 - personalInfo: - $ref: '#/components/schemas/BeneficialOwnerPersonalInfo' - kycStatus: - $ref: '#/components/schemas/KycStatus' - createdAt: + description: Grid-issued `Request:` identifier for this pending request. Echo this value exactly in the `Request-Id` header on the signed retry so the server can correlate the retry with the issued challenge. + example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: type: string format: date-time - description: When this beneficial owner was created - example: '2025-10-03T12:00:00Z' - updatedAt: + description: Timestamp after which this challenge is no longer valid. The signed retry must be submitted before this time. + example: '2026-04-08T15:35:00Z' + Error424: + type: object + required: + - message + - status + - code + properties: + status: + type: integer + enum: + - 424 + description: HTTP status code + code: type: string - format: date-time - description: When this beneficial owner was last updated - example: '2025-10-03T12:00:00Z' - BusinessCustomer: - title: Business Customer - allOf: - - $ref: '#/components/schemas/Customer' - - $ref: '#/components/schemas/BusinessCustomerFields' - - type: object - properties: - businessInfo: - $ref: '#/components/schemas/BusinessInfoResponse' - beneficialOwners: - type: array - items: - $ref: '#/components/schemas/BeneficialOwner' - CustomerOneOf: - oneOf: - - $ref: '#/components/schemas/IndividualCustomer' - - $ref: '#/components/schemas/BusinessCustomer' - discriminator: - propertyName: customerType - mapping: - INDIVIDUAL: '#/components/schemas/IndividualCustomer' - BUSINESS: '#/components/schemas/BusinessCustomer' - CustomerListResponse: + description: | + | Error Code | Description | + |------------|-------------| + | PAYREQ_REQUEST_FAILED | Payment request failed | + | COUNTERPARTY_PUBKEY_FETCH_ERROR | Error fetching counterparty public key | + | NO_COMPATIBLE_UMA_VERSION | No compatible UMA version | + | LNURLP_REQUEST_FAILED | LNURLP request failed | + | EMAIL_OTP_CREDENTIAL_SYNC_FAILED | Failed to update one or more tied EMAIL_OTP credentials | + enum: + - PAYREQ_REQUEST_FAILED + - COUNTERPARTY_PUBKEY_FETCH_ERROR + - NO_COMPATIBLE_UMA_VERSION + - LNURLP_REQUEST_FAILED + - EMAIL_OTP_CREDENTIAL_SYNC_FAILED + message: + type: string + description: Error message + details: + type: object + description: Additional error details + additionalProperties: true + KycLinkCreateRequest: + type: object + description: Request body for generating a hosted KYC link for an existing customer. + properties: + redirectUri: + type: string + format: uri + description: URI the customer is redirected to after completing the hosted KYC flow. Must start with `https://` (or `http://` for local development). Embedded in the returned `kycUrl`. + example: https://app.example.com/onboarding/completed + KycProvider: + type: string + description: The KYC provider that will perform identity verification for the customer. Grid selects the provider based on the customer's region and platform configuration; the value is informational for platforms that want to integrate directly with the provider's SDK. + enum: + - SUMSUB + example: SUMSUB + KycLinkResponse: type: object + description: A hosted KYC link that the customer can complete to verify their identity. required: - - data - - hasMore + - kycUrl + - expiresAt + - provider properties: - data: - type: array - description: List of customers matching the filter criteria - items: - $ref: '#/components/schemas/CustomerOneOf' - hasMore: - type: boolean - description: Indicates if more results are available beyond this page - nextCursor: + kycUrl: 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) - Error405: + description: Hosted URL the customer should be sent to in order to complete verification. The URL is single-use and expires at `expiresAt`. To generate a new link (for example, after the previous one expires or is abandoned), call this endpoint again. + example: https://kyc.lightspark.com/onboard/abc123def456 + expiresAt: + type: string + format: date-time + description: Time at which the hosted link expires and can no longer be used. + example: '2027-01-15T14:32:00Z' + provider: + $ref: '#/components/schemas/KycProvider' + token: + type: string + description: Provider-specific token that can be used in place of the hosted URL — for example, to embed the provider's SDK directly in your application. Only returned for providers that support direct SDK integration. Whether to use the hosted URL or the embedded SDK is up to you; both flows result in the same `kycStatus` update on the customer. + example: _act-sbx-jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + ContactVerificationConfirmRequest: type: object + description: Request body for confirming an email or phone verification challenge. required: - - message - - status - code properties: - status: - type: integer - enum: - - 405 - description: HTTP status code code: type: string - description: | - | Error Code | Description | - |------------|-------------| - | METHOD_NOT_ALLOWED | The HTTP method is not supported for this endpoint | - enum: - - METHOD_NOT_ALLOWED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true - CustomerCreateRequest: + description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`. + example: '123456' + ScaFactor: + type: string + enum: + - SMS_OTP + - TOTP + - PASSKEY + description: | + A Strong Customer Authentication factor. + + | Factor | Description | + |--------|-------------| + | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | + | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | + | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | + ScaFactorView: type: object + description: An enrolled Strong Customer Authentication factor. required: - - customerType + - factor properties: - platformCustomerId: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The kind of enrolled factor. + credentialId: type: string - description: Platform-specific customer identifier. If not provided, one will be generated by the system. - example: 9f84e0c2a72c4fa - customerType: - $ref: '#/components/schemas/CustomerType' - region: + description: The per-credential id, populated only for `PASSKEY` factors (the id passed to delete a passkey). Omitted for `TOTP` and `SMS_OTP`, which have no per-credential id. + name: type: string - description: Country code (ISO 3166-1 alpha-2) representing the customer's regional identity. This determines the regulatory jurisdiction and KYC requirements for the customer. Required if the customer will use currencies with different KYC requirements across regions. A customer with accounts in multiple regions should be registered as separate customers. This field is immutable after creation. - example: US - currencies: + description: An optional human-readable label for this factor. + ScaFactorList: + type: object + description: The Strong Customer Authentication factors a customer has enrolled. + required: + - factors + properties: + factors: type: array + description: The customer's enrolled SCA factors. items: - type: string - description: List of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Required if the customer will use more than one sending currency, since the correct currencies cannot always be inferred. If not provided, currencies will be inferred from the customer's region. Some currency combinations may require separate customers — if so, the request will be rejected with details. - example: - - USD - - USDC - email: - type: string - format: email - description: Email address for the customer. - example: john.doe@example.com - phoneNumber: - type: string - pattern: ^\+[1-9]\d{1,14}$ - description: Phone number for the customer in strict E.164 format. - example: '+14155551234' - umaAddress: + $ref: '#/components/schemas/ScaFactorView' + TotpFactorEnrollRequest: + type: object + title: TOTP Factor Enroll Request + description: Start enrolling a time-based one-time-password (TOTP) authenticator factor. + required: + - type + properties: + type: type: string - description: Optional UMA address identifier. If not provided during customer creation, one will be generated by the system. If provided during customer update, the UMA address will be updated to the provided value. This is an optional identifier to route payments to the customer. This is an optional identifier to route payments to the customer. - example: $john.doe@uma.domain.com - IndividualCustomerCreateRequest: - title: Individual Customer Create Request - allOf: - - $ref: '#/components/schemas/CustomerCreateRequest' - - $ref: '#/components/schemas/IndividualCustomerFields' - - type: object - properties: - idType: - $ref: '#/components/schemas/IdentificationType' - identifier: - type: string - writeOnly: true - description: The individual's tax identification number. Required to onboard the individual as a US account holder. Only SSN and ITIN are currently accepted for an individual account holder; other identification types are rejected. Write-only — never returned in customer responses. - example: 123-45-6789 - BusinessInfo: + enum: + - TOTP + description: Discriminator selecting the TOTP factor. TOTP enrollment needs no other input at start. + PasskeyFactorEnrollRequest: type: object - description: Additional information required for business entities + title: Passkey Factor Enroll Request + description: Start enrolling a WebAuthn passkey factor. required: - - legalName - - taxId - - incorporatedOn + - type properties: - legalName: + type: type: string - description: Legal name of the business - example: Acme Corporation, Inc. - doingBusinessAs: + enum: + - PASSKEY + description: Discriminator selecting the passkey factor. Passkey enrollment needs no other input at start. + ScaFactorEnrollRequestOneOf: + oneOf: + - $ref: '#/components/schemas/TotpFactorEnrollRequest' + - $ref: '#/components/schemas/PasskeyFactorEnrollRequest' + discriminator: + propertyName: type + mapping: + TOTP: '#/components/schemas/TotpFactorEnrollRequest' + PASSKEY: '#/components/schemas/PasskeyFactorEnrollRequest' + description: Which SCA factor to begin enrolling, selected by `type`. `SMS_OTP` is not enrollable (it uses the customer's verified phone), so only `TOTP` and `PASSKEY` are valid here. + TotpEnrollmentStart: + type: object + description: The shared secret a customer's authenticator app needs to enroll a TOTP factor. Returned by `POST /sca/factors` for a `TOTP` request; the customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with the first code their app produces. + required: + - type + - secret + - secretBase32Encoded + - totpUri + properties: + type: type: string - description: Trade name or DBA name of the business, if different from the legal name - example: Acme - country: + enum: + - TOTP + description: Discriminator identifying this as the TOTP enrollment-start payload. + secret: type: string - description: Country of incorporation or registration (ISO 3166-1 alpha-2) - example: US - registrationNumber: + description: The raw TOTP shared secret. + secretBase32Encoded: type: string - description: Business registration number - example: '5523041' - incorporatedOn: + description: The Base32-encoded shared secret, suitable for manual entry into an authenticator app that does not scan QR codes. + totpUri: type: string - format: date - description: Date of incorporation in ISO 8601 format (YYYY-MM-DD) - example: '2018-03-14' - entityType: - $ref: '#/components/schemas/EntityType' - taxId: + description: The `otpauth://` provisioning URI (the QR-code payload) the customer's authenticator app scans to enroll the factor. + example: otpauth://totp/Grid:customer@example.com?secret=ABC123&issuer=Grid + PasskeyEnrollmentStart: + type: object + description: Opaque WebAuthn registration options relayed to the end user's device to enroll a passkey factor. Grid performs no crypto; pass `options` to the device's WebAuthn API to produce a credential, then submit that credential to the confirm endpoint unmodified. + required: + - type + - options + - allowedOrigins + - relyingPartyId + properties: + type: type: string - description: Tax identification number - example: 47-1234567 - countriesOfOperation: + enum: + - PASSKEY + description: Discriminator identifying this as the passkey enrollment-start payload. + options: + type: object + additionalProperties: true + description: Opaque WebAuthn `PublicKeyCredentialCreationOptions`. Pass to the device's WebAuthn registration API unmodified. + allowedOrigins: type: array + description: The origins the WebAuthn registration ceremony may run against. The origin the credential is produced against must be one of these and must be echoed back on the confirm call. items: type: string - description: List of countries where the business operates (ISO 3166-1 alpha-2) example: - - US - businessType: - $ref: '#/components/schemas/BusinessType' - purposeOfAccount: - $ref: '#/components/schemas/PurposeOfAccount' - sourceOfFunds: + - https://app.example.com + relyingPartyId: type: string - description: The primary source of funds for the business - example: Funds derived from customer payments for software services - expectedMonthlyTransactionCount: + description: The WebAuthn relying-party id the credential is bound to. + example: app.example.com + ScaFactorEnrollStartOneOf: + oneOf: + - $ref: '#/components/schemas/TotpEnrollmentStart' + - $ref: '#/components/schemas/PasskeyEnrollmentStart' + discriminator: + propertyName: type + mapping: + TOTP: '#/components/schemas/TotpEnrollmentStart' + PASSKEY: '#/components/schemas/PasskeyEnrollmentStart' + description: 'The factor-specific material needed to complete enrollment, keyed by `type`: a TOTP shared secret + provisioning URI, or the WebAuthn registration options for a passkey.' + TotpEnrollmentConfirmRequest: + type: object + description: The shared secret returned by the TOTP enrollment start, plus the first code the customer's authenticator app produces, submitted to confirm and finalize the TOTP factor. + required: + - type + - secret + - code + properties: + type: type: string enum: - - COUNT_UNDER_10 - - COUNT_10_TO_100 - - COUNT_100_TO_500 - - COUNT_500_TO_1000 - - COUNT_OVER_1000 - description: Expected number of transactions per month - example: COUNT_100_TO_500 - expectedMonthlyTransactionVolume: + - TOTP + description: Discriminator selecting the TOTP confirm variant. + secret: type: string - enum: - - VOLUME_UNDER_10K - - VOLUME_10K_TO_100K - - VOLUME_100K_TO_1M - - VOLUME_1M_TO_10M - - VOLUME_OVER_10M - description: Expected total transaction volume per month in USD equivalent - example: VOLUME_100K_TO_1M - expectedRecipientJurisdictions: - type: array - items: - type: string - description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2) - example: - - US - naicsCode: + description: The shared secret returned as `secret` by the TOTP enrollment start, threaded back to bind the confirmation to that enrollment. + code: type: string - pattern: ^\d{2,6}$ - description: NAICS code describing the nature of the business (2-6 digits) - example: '541511' - sourceOfFundsCategories: - type: array - items: - $ref: '#/components/schemas/SourceOfFundsCategory' - description: Structured source-of-funds categories for the business - example: - - OPERATING_REVENUE - sourceOfFundsOtherDescription: + description: The current time-based one-time code from the customer's authenticator app. In sandbox, the code is always `123456`. + example: '123456' + PasskeyEnrollmentConfirmRequest: + type: object + description: The WebAuthn credential a device produced for a passkey registration challenge, submitted to enroll the passkey factor. + required: + - type + - origin + - credential + properties: + type: type: string - minLength: 1 - maxLength: 500 - description: Description of the source of funds when OTHER is selected - example: Proceeds from a legal settlement - purposeOfAccountOtherDescription: + enum: + - PASSKEY + description: Discriminator selecting the passkey confirm variant. + origin: + type: string + description: The WebAuthn origin the `credential` was produced against (one of the enrollment start's `allowedOrigins`). + example: https://app.example.com + credential: + type: object + additionalProperties: true + description: Opaque WebAuthn credential the device produced from the enrollment start's `options`. + ScaFactorConfirmRequestOneOf: + oneOf: + - $ref: '#/components/schemas/TotpEnrollmentConfirmRequest' + - $ref: '#/components/schemas/PasskeyEnrollmentConfirmRequest' + discriminator: + propertyName: type + mapping: + TOTP: '#/components/schemas/TotpEnrollmentConfirmRequest' + PASSKEY: '#/components/schemas/PasskeyEnrollmentConfirmRequest' + description: 'The proof that finalizes enrollment, keyed by `type`: the TOTP shared secret + code, or the passkey `origin` + `credential`.' + TotpEnrollmentConfirmResponse: + type: object + description: The one-time recovery codes issued once a TOTP factor is enrolled. These are shown to the customer only once; store them somewhere safe to recover access if the authenticator device is lost. + required: + - type + - recoveryCodes + properties: + type: type: string - minLength: 1 - maxLength: 500 - description: Description of the account purpose when OTHER is selected - example: Escrow for equipment leases - expectedCounterpartyCountries: + enum: + - TOTP + description: Discriminator identifying this as the TOTP enrollment result. + recoveryCodes: type: array + description: The one-time recovery codes for this TOTP factor. items: type: string - description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2) example: - - US - BusinessCustomerCreateRequest: - title: Business Customer Create Request - allOf: - - $ref: '#/components/schemas/CustomerCreateRequest' - - $ref: '#/components/schemas/BusinessCustomerFields' - - type: object - properties: - businessInfo: - $ref: '#/components/schemas/BusinessInfo' - CustomerCreateRequestOneOf: + - ABCD-EFGH-IJKL + - MNOP-QRST-UVWX + PasskeyEnrollmentConfirmResponse: + type: object + description: The enrolled passkey factor returned after a successful confirmation. + required: + - type + - factor + properties: + type: + type: string + enum: + - PASSKEY + description: Discriminator identifying this as the passkey enrollment result. + factor: + $ref: '#/components/schemas/ScaFactorView' + ScaFactorConfirmResponseOneOf: oneOf: - - $ref: '#/components/schemas/IndividualCustomerCreateRequest' - - $ref: '#/components/schemas/BusinessCustomerCreateRequest' + - $ref: '#/components/schemas/TotpEnrollmentConfirmResponse' + - $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse' discriminator: - propertyName: customerType + propertyName: type mapping: - INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest' - BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest' - Error409: + TOTP: '#/components/schemas/TotpEnrollmentConfirmResponse' + PASSKEY: '#/components/schemas/PasskeyEnrollmentConfirmResponse' + description: The enrollment result, keyed by `type`. + ScaLoginStartRequest: type: object + description: Selects which enrolled factor to start an SCA login with. The factor must already be enrolled (or, for `SMS_OTP`, the phone verified). required: - - message - - status - - code + - factor properties: - status: - type: integer - enum: - - 409 - description: HTTP status code + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor to authenticate with. + ScaLoginStart: + type: object + description: 'The factor-specific material a customer needs to complete an SCA login. Each factor surfaces only the fields it issues: `SMS_OTP` carries `challengeId` and `expiresAt`; `TOTP` carries neither (the customer reads the code from their authenticator app); `PASSKEY` carries the opaque WebAuthn `passkeyOptions` with `allowedOrigins` and `relyingPartyId`.' + required: + - factor + properties: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor this login was started for. + challengeId: + type: + - string + - 'null' + description: The challenge handle for an `SMS_OTP` login, threaded back on the complete call. Present only for `SMS_OTP`. + expiresAt: + type: + - string + - 'null' + format: date-time + description: Absolute UTC timestamp after which the `SMS_OTP` code expires. Present only for `SMS_OTP`. + example: '2025-10-03T12:05:00Z' + passkeyOptions: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion request options. Present only for `PASSKEY`; pass to the device's WebAuthn API to produce the assertion submitted on the complete call. + allowedOrigins: + type: + - array + - 'null' + items: + type: string + description: The origins the WebAuthn ceremony may run against. Present only for `PASSKEY`. + example: + - https://app.example.com + relyingPartyId: + type: + - string + - 'null' + description: The WebAuthn relying-party id. Present only for `PASSKEY`. + example: app.example.com + ScaLoginCompleteRequest: + type: object + description: Completes an SCA login by submitting the proof for the started factor. Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`), plus the `factor` being completed and, for `SMS_OTP`, the `challengeId` returned by the login start. + required: + - factor + anyOf: + - required: + - code + - required: + - passkeyAssertion + - origin + properties: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor being completed; must match the started login. + challengeId: + type: + - string + - 'null' + description: The challenge handle returned by the login start, required for `SMS_OTP` and omitted for other factors. code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL | Transaction is not pending platform approval | - | UMA_ADDRESS_EXISTS | UMA address already exists | - | EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential | - | EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued | - | CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform | - enum: - - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL - - UMA_ADDRESS_EXISTS - - EMAIL_OTP_EMAIL_ALREADY_EXISTS - - EMAIL_OTP_CREDENTIAL_SET_CHANGED - - CONFLICT - message: - type: string - description: Error message - details: - type: object - description: Additional error details + type: + - string + - 'null' + description: The one-time code the customer received by SMS, or read from their authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is always `123456`. + example: '123456' + passkeyAssertion: + type: + - object + - 'null' additionalProperties: true - Error404: + description: Opaque WebAuthn assertion produced by the device from the login start's `passkeyOptions`. Required when completing a `PASSKEY` login. + origin: + type: + - string + - 'null' + description: The WebAuthn origin the `passkeyAssertion` was produced against (one of the login start's `allowedOrigins`). Required alongside `passkeyAssertion`; omit it for the `code` path. + example: https://app.example.com + ScaLoginComplete: type: object + description: The status of a completed SCA login session. required: - - message - status - - code properties: status: - type: integer - enum: - - 404 - description: HTTP status code - code: type: string - description: | - | Error Code | Description | - |------------|-------------| - | TRANSACTION_NOT_FOUND | Transaction not found | - | INVITATION_NOT_FOUND | Invitation not found | - | USER_NOT_FOUND | Customer not found | - | QUOTE_NOT_FOUND | Quote not found | - | LOOKUP_REQUEST_NOT_FOUND | Lookup request not found | - | TOKEN_NOT_FOUND | Token not found | - | BULK_UPLOAD_JOB_NOT_FOUND | Bulk upload job not found | - | REFERENCE_NOT_FOUND | Reference not found | - | UMA_NOT_FOUND | The UMA address is well-formed but no receiver exists at the counterparty VASP | - | STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND | Stablecoin provider account link not found | - enum: - - TRANSACTION_NOT_FOUND - - INVITATION_NOT_FOUND - - USER_NOT_FOUND - - QUOTE_NOT_FOUND - - LOOKUP_REQUEST_NOT_FOUND - - TOKEN_NOT_FOUND - - BULK_UPLOAD_JOB_NOT_FOUND - - REFERENCE_NOT_FOUND - - UMA_NOT_FOUND - - STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true - Error410: + description: The status of the login session, passed through verbatim (Grid does not normalize it). A successful login reports `SUCCESS`; other values indicate the login did not complete and should be surfaced to the caller. + example: SUCCESS + Error423: type: object required: - message @@ -10793,97 +12217,117 @@ components: status: type: integer enum: - - 410 + - 423 description: HTTP status code code: type: string description: | | Error Code | Description | |------------|-------------| - | CUSTOMER_DELETED | Customer has been permanently deleted | + | ACCOUNT_LOCKED | The customer's login is temporarily locked (or suspended) after too many failed attempts. Inspect `details.lockedUntil` for when the customer may retry, and `details.failedAttempts` for the current count. | enum: - - CUSTOMER_DELETED + - ACCOUNT_LOCKED message: type: string description: Error message details: type: object - description: Additional error details + description: Lockout detail. `lockedUntil` is the UTC timestamp the customer may retry after (absent when suspended indefinitely); `failedAttempts` is the current cumulative failed-login count; `suspended` is true when the account is locked with no automatic expiry. + properties: + suspended: + type: boolean + lockedUntil: + type: + - string + - 'null' + format: date-time + failedAttempts: + type: integer additionalProperties: true - CustomerUpdateRequest: - title: Customer Update Request - description: Request body for `PATCH /customers/{customerId}`. When `email` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer email and every tied `EMAIL_OTP` credential through the endpoint's signed-retry flow. When `phoneNumber` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer phone number and every tied `SMS_OTP` credential through the same signed-retry flow. Update `email` and `phoneNumber` in separate PATCH calls. + RecordSecurityEventRequest: type: object + description: Records a client-side security event for the customer so Grid can maintain the customer's login-security state (SCA session revocation and failed-login lockout). required: - - customerType + - eventType properties: - customerType: - $ref: '#/components/schemas/CustomerType' - currencies: - type: array - items: - type: string - description: Updated list of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Replaces the existing list. Some currency combinations may require separate customers — if so, the request will be rejected with details. - example: - - USD - - EUR - - USDC - email: - type: string - format: email - description: Email address for the customer. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `EMAIL_OTP` credential across all tied Embedded Wallets. - example: john.doe@example.com - phoneNumber: + eventType: type: string - pattern: ^\+[1-9]\d{1,14}$ - description: Phone number for the customer in strict E.164 format. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `SMS_OTP` credential across all tied Embedded Wallets. Send phone number and email updates as separate PATCH calls. - example: '+14155551234' - umaAddress: - type: string - description: Optional UMA address identifier. If provided, the customer's UMA address will be updated. This is an optional identifier to route payments to the customer. - example: $john.doe@uma.domain.com - IndividualCustomerUpdateRequest: - title: Individual Customer Update Request - allOf: - - $ref: '#/components/schemas/CustomerUpdateRequest' - - $ref: '#/components/schemas/IndividualCustomerFields' - BusinessCustomerUpdateRequest: - title: Business Customer Update Request - allOf: - - $ref: '#/components/schemas/CustomerUpdateRequest' - - $ref: '#/components/schemas/BusinessCustomerFields' - CustomerUpdateRequestOneOf: - oneOf: - - $ref: '#/components/schemas/IndividualCustomerUpdateRequest' - - $ref: '#/components/schemas/BusinessCustomerUpdateRequest' - discriminator: - propertyName: customerType - mapping: - INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' - BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' - SignedRequestChallenge: - title: Signed Request Challenge + enum: + - RESET_PASSWORD_COMPLETED + - FAILED_LOGIN_ATTEMPT + description: | + The security event to record: + + | Value | Effect | + |-------|--------| + | `RESET_PASSWORD_COMPLETED` | Revokes every active SCA session for the customer and clears the failed-login counter. | + | `FAILED_LOGIN_ATTEMPT` | Increments the cumulative failed-login counter and escalates the lockout at each milestone: 5 attempts → 15 minutes, 6 → 30 minutes, 7 → 1 hour, 8 → 24 hours, 9 or more → suspension. | + example: FAILED_LOGIN_ATTEMPT + RecordSecurityEventResponse: type: object + description: The customer's login-security state after recording the event, so the integrator can surface a lockout to the end user. required: - - payloadToSign - - requestId - - expiresAt - description: Common base for two-step signed-retry challenge responses on Embedded Wallet endpoints (credential registration or revocation, session refresh or revocation, wallet export, customer email updates, and similar). Holds the signing fields shared across every challenge shape; each variant composes this base via `allOf` and adds its own resource `id` (and `type`, when applicable) with variant-specific description and example. + - eventType + - suspended + - failedAttempts properties: - payloadToSign: + eventType: type: string - description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request. - example: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_EXAMPLE"}' - requestId: + enum: + - RESET_PASSWORD_COMPLETED + - FAILED_LOGIN_ATTEMPT + description: The event that was recorded. + example: FAILED_LOGIN_ATTEMPT + suspended: + type: boolean + description: Whether the customer's login is currently suspended (locked with no automatic expiry). A suspended customer must go through a password reset (`RESET_PASSWORD_COMPLETED`) to clear the lockout. + lockedUntil: + type: + - string + - 'null' + format: date-time + description: When the customer may attempt to log in again, if temporarily locked. Null when not locked, or when `suspended` is true (no automatic expiry). + example: '2025-10-03T12:15:00Z' + failedAttempts: + type: integer + description: The customer's current cumulative failed-login count. + example: 5 + TwoFactorResetStartRequest: + type: object + description: Selects which enrolled factor to reset via the liveness-gated recovery flow. + required: + - factor + properties: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The enrolled factor to reset. + TwoFactorResetStart: + type: object + description: The reset handle plus the opaque liveness handles a caller relays to the end-user device to complete the liveness check. `resetId` threads the ceremony together (status and complete reference it). `livenessAccessToken` and `verificationLink` are omitted when they are not returned. + required: + - resetId + properties: + resetId: type: string - description: Grid-issued `Request:` identifier for this pending request. Echo this value exactly in the `Request-Id` header on the signed retry so the server can correlate the retry with the issued challenge. - example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + description: Identifier for this reset; pass it to the status and complete endpoints. + livenessAccessToken: + type: + - string + - 'null' + description: Access token for the embedded liveness/verification SDK, bound to this reset. Omitted when one is not returned. + verificationLink: + type: + - string + - 'null' + description: Hosted identity-verification page URL for completing liveness. Omitted when one is not returned. expiresAt: - type: string + type: + - string + - 'null' format: date-time - description: Timestamp after which this challenge is no longer valid. The signed retry must be submitted before this time. - example: '2026-04-08T15:35:00Z' - Error424: + description: Absolute UTC timestamp at the end of the reset window. Omitted when one is not returned. + example: '2025-10-03T12:30:00Z' + Error429: type: object required: - message @@ -10893,24 +12337,16 @@ components: status: type: integer enum: - - 424 + - 429 description: HTTP status code code: type: string description: | | Error Code | Description | |------------|-------------| - | PAYREQ_REQUEST_FAILED | Payment request failed | - | COUNTERPARTY_PUBKEY_FETCH_ERROR | Error fetching counterparty public key | - | NO_COMPATIBLE_UMA_VERSION | No compatible UMA version | - | LNURLP_REQUEST_FAILED | LNURLP request failed | - | EMAIL_OTP_CREDENTIAL_SYNC_FAILED | Failed to update one or more tied EMAIL_OTP credentials | + | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header | enum: - - PAYREQ_REQUEST_FAILED - - COUNTERPARTY_PUBKEY_FETCH_ERROR - - NO_COMPATIBLE_UMA_VERSION - - LNURLP_REQUEST_FAILED - - EMAIL_OTP_CREDENTIAL_SYNC_FAILED + - RATE_LIMITED message: type: string description: Error message @@ -10918,54 +12354,175 @@ components: type: object description: Additional error details additionalProperties: true - KycLinkCreateRequest: + TwoFactorResetStatus: type: object - description: Request body for generating a hosted KYC link for an existing customer. + description: The status of an in-progress 2FA reset, polled until it reaches a terminal value. + required: + - status + - factor + - expiresAt properties: - redirectUri: + status: type: string - format: uri - description: URI the customer is redirected to after completing the hosted KYC flow. Must start with `https://` (or `http://` for local development). Embedded in the returned `kycUrl`. - example: https://app.example.com/onboarding/completed - KycProvider: - type: string - description: The KYC provider that will perform identity verification for the customer. Grid selects the provider based on the customer's region and platform configuration; the value is informational for platforms that want to integrate directly with the provider's SDK. - enum: - - SUMSUB - example: SUMSUB - KycLinkResponse: + enum: + - INITIATED + - PENDING_REVIEW + - LIVENESS_PASSED + - COMPLETED + - REJECTED + - EXPIRED + description: |- + The reset status. + | Value | Terminal | Meaning | |-------|----------|---------| | `INITIATED` | no | Reset started; liveness not yet submitted. Keep polling. | | `PENDING_REVIEW` | no | Liveness submitted; under review. Keep polling. | | `LIVENESS_PASSED` | no | Liveness passed; call the complete endpoint to finish the reset. | | `COMPLETED` | **yes** | Reset finished; the lost factor is cleared and re-enrollment can begin. | | `REJECTED` | **yes** | Liveness failed. Stop polling; start a new reset. | | `EXPIRED` | **yes** | The reset window closed before it completed. Stop polling; start a new reset. | + Stop polling on any terminal value. + example: INITIATED + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor being reset. + enrollmentStatus: + type: + - string + - 'null' + enum: + - PENDING + - COMPLETED + - null + description: The enrollment status of the replacement factor. `PENDING` until the customer finishes re-enrolling, then `COMPLETED`. Null for an `SMS_OTP` reset, where completing the reset enrolls the new number directly with no separate enrollment step. + example: PENDING + expiresAt: + type: string + format: date-time + description: When the reset window closes. Poll no longer than this. + example: '2025-10-03T12:15:00Z' + completedAt: + type: + - string + - 'null' + format: date-time + description: When the reset completed; null until then. + example: null + TwoFactorResetCompleteRequest: type: object - description: A hosted KYC link that the customer can complete to verify their identity. + description: Optional body for completing a 2FA reset. Only needed when resetting the `SMS_OTP` factor to a new phone number; omit the body entirely otherwise. + properties: + mobile: + type: object + description: For an `SMS_OTP` reset, the new mobile number to enroll as the customer completes the reset. Required for an `SMS_OTP` reset; ignored for `TOTP` / `PASSKEY` resets. + required: + - countryCode + - number + properties: + countryCode: + type: string + description: The country dialing code, including the leading `+`. + example: '+1' + number: + type: string + description: The national subscriber number, without the country code. + example: '4155550123' + ScaChallenge: + type: object + description: |- + A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. + + When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`. + + **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. required: - - kycUrl + - id - expiresAt - - provider + - factor + - availableFactors properties: - kycUrl: + id: type: string - description: Hosted URL the customer should be sent to in order to complete verification. The URL is single-use and expires at `expiresAt`. To generate a new link (for example, after the previous one expires or is abandoned), call this endpoint again. - example: https://kyc.lightspark.com/onboard/abc123def456 + description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). + example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 expiresAt: type: string format: date-time - description: Time at which the hosted link expires and can no longer be used. - example: '2027-01-15T14:32:00Z' - provider: - $ref: '#/components/schemas/KycProvider' - token: - type: string - description: Provider-specific token that can be used in place of the hosted URL — for example, to embed the provider's SDK directly in your application. Only returned for providers that support direct SDK integration. Whether to use the hosted URL or the embedded SDK is up to you; both flows result in the same `kycStatus` update on the customer. - example: _act-sbx-jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - ContactVerificationConfirmRequest: + description: Absolute UTC timestamp after which this challenge can no longer be authorized. + example: '2025-10-03T12:05:00Z' + factor: + $ref: '#/components/schemas/ScaFactor' + description: The factor this challenge was issued for. Defaults to `SMS_OTP`. + availableFactors: + type: array + description: The factors the customer may use to satisfy this challenge. + items: + $ref: '#/components/schemas/ScaFactor' + example: + - SMS_OTP + purpose: + type: + - string + - 'null' + description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). + example: PAYOUT + passkeyAssertionOptions: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. + passkeyAllowedOrigins: + type: + - array + - 'null' + items: + type: string + description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. + example: + - https://app.example.com + BeneficiaryTrustStart: type: object - description: Request body for confirming an email or phone verification challenge. - required: - - code + description: The SCA challenge (if any) a caller authorizes to finish trusting (or untrusting) a beneficiary. The beneficiary is identified by its `externalAccountId`, so the confirm call needs no separate handle. `scaChallenge` is omitted when no challenge is issued; the caller then confirms without a `challengeId`. + properties: + scaChallenge: + $ref: '#/components/schemas/ScaChallenge' + description: The SCA challenge to satisfy on the confirm call. Omitted when no challenge is issued. + BeneficiaryTrustConfirmRequest: + type: object + description: Confirms trusting or untrusting a beneficiary by submitting the SCA proof. Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`) and, when the start issued one, the `challengeId`. The beneficiary is identified by the `externalAccountId` in the path — no separate handle is needed. + anyOf: + - required: + - code + - required: + - passkeyAssertion + - origin properties: + challengeId: + type: + - string + - 'null' + description: The challenge handle from the trust start's `scaChallenge`, when one was issued. Omit when the start returned no challenge. code: - type: string - description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`. + type: + - string + - 'null' + description: The one-time code the customer received by SMS, or read from their authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is always `123456`. example: '123456' + passkeyAssertion: + type: + - object + - 'null' + additionalProperties: true + description: Opaque WebAuthn assertion produced by the device from the challenge's assertion options. Required when satisfying a `PASSKEY` challenge. + origin: + type: + - string + - 'null' + description: The WebAuthn origin the `passkeyAssertion` was produced against. Required alongside `passkeyAssertion`; omit it for the `code` path. + example: https://app.example.com + BeneficiaryTrustConfirm: + type: object + description: The result of a confirm-trust / confirm-untrust call. `trusted` is `true` after a successful trust and `false` after a successful untrust. + required: + - trusted + properties: + trusted: + type: boolean + description: Whether the beneficiary is now trusted. `true` after a successful trust, `false` after a successful untrust. InternalAccountType: title: Internal Account Type type: string @@ -18202,20 +19759,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' - ScaFactor: - type: string - enum: - - SMS_OTP - - TOTP - - PASSKEY - description: | - A Strong Customer Authentication factor. - - | Factor | Description | - |--------|-------------| - | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. | - | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). | - | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. | TransferOutRequest: type: object required: @@ -18238,9 +19781,6 @@ components: maxLength: 80 description: 'Free-form information about the payment that travels with it 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.' example: '12345' - scaFactor: - $ref: '#/components/schemas/ScaFactor' - description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`. CurrencyPreference: type: object required: @@ -18475,60 +20015,6 @@ components: mapping: ACCOUNT: '#/components/schemas/AccountDestination' UMA_ADDRESS: '#/components/schemas/UmaAddressDestination' - ScaChallenge: - type: object - description: |- - A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed. - - When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`. - - **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged. - required: - - id - - expiresAt - - factor - - availableFactors - properties: - id: - type: string - description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation). - example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007 - expiresAt: - type: string - format: date-time - description: Absolute UTC timestamp after which this challenge can no longer be authorized. - example: '2025-10-03T12:05:00Z' - factor: - $ref: '#/components/schemas/ScaFactor' - description: The factor this challenge was issued for. Defaults to `SMS_OTP`. - availableFactors: - type: array - description: The factors the customer may use to satisfy this challenge. - items: - $ref: '#/components/schemas/ScaFactor' - example: - - SMS_OTP - purpose: - type: - - string - - 'null' - description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow). - example: PAYOUT - passkeyAssertionOptions: - type: - - object - - 'null' - additionalProperties: true - description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`. - passkeyAllowedOrigins: - type: - - array - - 'null' - items: - type: string - description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case. - example: - - https://app.example.com Quote: type: object required: @@ -18753,33 +20239,6 @@ components: - 'null' description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`. example: https://app.example.com - Error429: - type: object - required: - - message - - status - - code - properties: - status: - type: integer - enum: - - 429 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header | - enum: - - RATE_LIMITED - message: - type: string - description: Error message - details: - type: object - description: Additional error details - additionalProperties: true TransactionListResponse: type: object required: diff --git a/openapi/components/schemas/errors/Error409.yaml b/openapi/components/schemas/errors/Error409.yaml index 09db54cbe..4bc53ad27 100644 --- a/openapi/components/schemas/errors/Error409.yaml +++ b/openapi/components/schemas/errors/Error409.yaml @@ -18,12 +18,14 @@ properties: | UMA_ADDRESS_EXISTS | UMA address already exists | | EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential | | EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued | + | PASSKEY_ALREADY_ENROLLED | The customer already has an enrolled passkey factor; only one passkey per customer is supported. Delete the existing one before enrolling another | | CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform | enum: - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL - UMA_ADDRESS_EXISTS - EMAIL_OTP_EMAIL_ALREADY_EXISTS - EMAIL_OTP_CREDENTIAL_SET_CHANGED + - PASSKEY_ALREADY_ENROLLED - CONFLICT message: type: string diff --git a/openapi/components/schemas/errors/Error423.yaml b/openapi/components/schemas/errors/Error423.yaml new file mode 100644 index 000000000..63d80498b --- /dev/null +++ b/openapi/components/schemas/errors/Error423.yaml @@ -0,0 +1,40 @@ +type: object +required: + - message + - status + - code +properties: + status: + type: integer + enum: + - 423 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | ACCOUNT_LOCKED | The customer's login is temporarily locked (or suspended) after too many failed attempts. Inspect `details.lockedUntil` for when the customer may retry, and `details.failedAttempts` for the current count. | + enum: + - ACCOUNT_LOCKED + message: + type: string + description: Error message + details: + type: object + description: >- + Lockout detail. `lockedUntil` is the UTC timestamp the customer may retry + after (absent when suspended indefinitely); `failedAttempts` is the current + cumulative failed-login count; `suspended` is true when the account is + locked with no automatic expiry. + properties: + suspended: + type: boolean + lockedUntil: + type: + - string + - 'null' + format: date-time + failedAttempts: + type: integer + additionalProperties: true diff --git a/openapi/components/schemas/sca/BeneficiaryTrustConfirm.yaml b/openapi/components/schemas/sca/BeneficiaryTrustConfirm.yaml new file mode 100644 index 000000000..beb40af35 --- /dev/null +++ b/openapi/components/schemas/sca/BeneficiaryTrustConfirm.yaml @@ -0,0 +1,12 @@ +type: object +description: >- + The result of a confirm-trust / confirm-untrust call. `trusted` is `true` + after a successful trust and `false` after a successful untrust. +required: + - trusted +properties: + trusted: + type: boolean + description: >- + Whether the beneficiary is now trusted. `true` after a successful trust, + `false` after a successful untrust. diff --git a/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml new file mode 100644 index 000000000..686da5819 --- /dev/null +++ b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml @@ -0,0 +1,46 @@ +type: object +description: >- + Confirms trusting or untrusting a beneficiary by submitting the SCA proof. + Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / + `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`) and, when the start + issued one, the `challengeId`. The beneficiary is identified by the + `externalAccountId` in the path — no separate handle is needed. +anyOf: + - required: + - code + - required: + - passkeyAssertion + - origin +properties: + challengeId: + type: + - string + - 'null' + description: >- + The challenge handle from the trust start's `scaChallenge`, when one was + issued. Omit when the start returned no challenge. + code: + type: + - string + - 'null' + description: >- + The one-time code the customer received by SMS, or read from their + authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is + always `123456`. + example: '123456' + passkeyAssertion: + type: + - object + - 'null' + additionalProperties: true + description: >- + Opaque WebAuthn assertion produced by the device from the challenge's + assertion options. Required when satisfying a `PASSKEY` challenge. + origin: + type: + - string + - 'null' + description: >- + The WebAuthn origin the `passkeyAssertion` was produced against. Required + alongside `passkeyAssertion`; omit it for the `code` path. + example: https://app.example.com diff --git a/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml b/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml new file mode 100644 index 000000000..85d2565d6 --- /dev/null +++ b/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml @@ -0,0 +1,13 @@ +type: object +description: >- + The SCA challenge (if any) a caller authorizes to finish trusting (or + untrusting) a beneficiary. The beneficiary is identified by its + `externalAccountId`, so the confirm call needs no separate handle. + `scaChallenge` is omitted when no challenge is issued; the caller then confirms + without a `challengeId`. +properties: + scaChallenge: + $ref: ./ScaChallenge.yaml + description: >- + The SCA challenge to satisfy on the confirm call. Omitted when no challenge + is issued. diff --git a/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml new file mode 100644 index 000000000..8a514ff86 --- /dev/null +++ b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml @@ -0,0 +1,26 @@ +type: object +description: >- + The WebAuthn credential a device produced for a passkey registration + challenge, submitted to enroll the passkey factor. +required: + - type + - origin + - credential +properties: + type: + type: string + enum: + - PASSKEY + description: Discriminator selecting the passkey confirm variant. + origin: + type: string + description: >- + The WebAuthn origin the `credential` was produced against (one of the + enrollment start's `allowedOrigins`). + example: https://app.example.com + credential: + type: object + additionalProperties: true + description: >- + Opaque WebAuthn credential the device produced from the enrollment start's + `options`. diff --git a/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml new file mode 100644 index 000000000..b74b86298 --- /dev/null +++ b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml @@ -0,0 +1,13 @@ +type: object +description: The enrolled passkey factor returned after a successful confirmation. +required: + - type + - factor +properties: + type: + type: string + enum: + - PASSKEY + description: Discriminator identifying this as the passkey enrollment result. + factor: + $ref: ./ScaFactorView.yaml diff --git a/openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml b/openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml new file mode 100644 index 000000000..952799450 --- /dev/null +++ b/openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml @@ -0,0 +1,37 @@ +type: object +description: >- + Opaque WebAuthn registration options relayed to the end user's device to + enroll a passkey factor. Grid performs no crypto; pass `options` to the + device's WebAuthn API to produce a credential, then submit that credential to + the confirm endpoint unmodified. +required: + - type + - options + - allowedOrigins + - relyingPartyId +properties: + type: + type: string + enum: + - PASSKEY + description: Discriminator identifying this as the passkey enrollment-start payload. + options: + type: object + additionalProperties: true + description: >- + Opaque WebAuthn `PublicKeyCredentialCreationOptions`. Pass to the device's + WebAuthn registration API unmodified. + allowedOrigins: + type: array + description: >- + The origins the WebAuthn registration ceremony may run against. The origin + the credential is produced against must be one of these and must be echoed + back on the confirm call. + items: + type: string + example: + - https://app.example.com + relyingPartyId: + type: string + description: The WebAuthn relying-party id the credential is bound to. + example: app.example.com diff --git a/openapi/components/schemas/sca/PasskeyFactorEnrollRequest.yaml b/openapi/components/schemas/sca/PasskeyFactorEnrollRequest.yaml new file mode 100644 index 000000000..121788885 --- /dev/null +++ b/openapi/components/schemas/sca/PasskeyFactorEnrollRequest.yaml @@ -0,0 +1,11 @@ +type: object +title: Passkey Factor Enroll Request +description: Start enrolling a WebAuthn passkey factor. +required: + - type +properties: + type: + type: string + enum: + - PASSKEY + description: Discriminator selecting the passkey factor. Passkey enrollment needs no other input at start. diff --git a/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml new file mode 100644 index 000000000..0ba668d80 --- /dev/null +++ b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml @@ -0,0 +1,21 @@ +type: object +description: >- + Records a client-side security event for the customer so Grid can maintain the + customer's login-security state (SCA session revocation and failed-login + lockout). +required: + - eventType +properties: + eventType: + type: string + enum: + - RESET_PASSWORD_COMPLETED + - FAILED_LOGIN_ATTEMPT + description: | + The security event to record: + + | Value | Effect | + |-------|--------| + | `RESET_PASSWORD_COMPLETED` | Revokes every active SCA session for the customer and clears the failed-login counter. | + | `FAILED_LOGIN_ATTEMPT` | Increments the cumulative failed-login counter and escalates the lockout at each milestone: 5 attempts → 15 minutes, 6 → 30 minutes, 7 → 1 hour, 8 → 24 hours, 9 or more → suspension. | + example: FAILED_LOGIN_ATTEMPT diff --git a/openapi/components/schemas/sca/RecordSecurityEventResponse.yaml b/openapi/components/schemas/sca/RecordSecurityEventResponse.yaml new file mode 100644 index 000000000..b0e9a0947 --- /dev/null +++ b/openapi/components/schemas/sca/RecordSecurityEventResponse.yaml @@ -0,0 +1,35 @@ +type: object +description: >- + The customer's login-security state after recording the event, so the + integrator can surface a lockout to the end user. +required: + - eventType + - suspended + - failedAttempts +properties: + eventType: + type: string + enum: + - RESET_PASSWORD_COMPLETED + - FAILED_LOGIN_ATTEMPT + description: The event that was recorded. + example: FAILED_LOGIN_ATTEMPT + suspended: + type: boolean + description: >- + Whether the customer's login is currently suspended (locked with no + automatic expiry). A suspended customer must go through a password reset + (`RESET_PASSWORD_COMPLETED`) to clear the lockout. + lockedUntil: + type: + - string + - 'null' + format: date-time + description: >- + When the customer may attempt to log in again, if temporarily locked. + Null when not locked, or when `suspended` is true (no automatic expiry). + example: '2025-10-03T12:15:00Z' + failedAttempts: + type: integer + description: The customer's current cumulative failed-login count. + example: 5 diff --git a/openapi/components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml b/openapi/components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml new file mode 100644 index 000000000..e8b9cbcba --- /dev/null +++ b/openapi/components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml @@ -0,0 +1,11 @@ +oneOf: + - $ref: ./TotpEnrollmentConfirmRequest.yaml + - $ref: ./PasskeyEnrollmentConfirmRequest.yaml +discriminator: + propertyName: type + mapping: + TOTP: ./TotpEnrollmentConfirmRequest.yaml + PASSKEY: ./PasskeyEnrollmentConfirmRequest.yaml +description: >- + The proof that finalizes enrollment, keyed by `type`: the TOTP shared secret + + code, or the passkey `origin` + `credential`. diff --git a/openapi/components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml b/openapi/components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml new file mode 100644 index 000000000..021804cd9 --- /dev/null +++ b/openapi/components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml @@ -0,0 +1,9 @@ +oneOf: + - $ref: ./TotpEnrollmentConfirmResponse.yaml + - $ref: ./PasskeyEnrollmentConfirmResponse.yaml +discriminator: + propertyName: type + mapping: + TOTP: ./TotpEnrollmentConfirmResponse.yaml + PASSKEY: ./PasskeyEnrollmentConfirmResponse.yaml +description: The enrollment result, keyed by `type`. diff --git a/openapi/components/schemas/sca/ScaFactorEnrollRequestOneOf.yaml b/openapi/components/schemas/sca/ScaFactorEnrollRequestOneOf.yaml new file mode 100644 index 000000000..d464c3aab --- /dev/null +++ b/openapi/components/schemas/sca/ScaFactorEnrollRequestOneOf.yaml @@ -0,0 +1,12 @@ +oneOf: + - $ref: ./TotpFactorEnrollRequest.yaml + - $ref: ./PasskeyFactorEnrollRequest.yaml +discriminator: + propertyName: type + mapping: + TOTP: ./TotpFactorEnrollRequest.yaml + PASSKEY: ./PasskeyFactorEnrollRequest.yaml +description: >- + Which SCA factor to begin enrolling, selected by `type`. `SMS_OTP` is not + enrollable (it uses the customer's verified phone), so only `TOTP` and + `PASSKEY` are valid here. diff --git a/openapi/components/schemas/sca/ScaFactorEnrollStartOneOf.yaml b/openapi/components/schemas/sca/ScaFactorEnrollStartOneOf.yaml new file mode 100644 index 000000000..b216b393c --- /dev/null +++ b/openapi/components/schemas/sca/ScaFactorEnrollStartOneOf.yaml @@ -0,0 +1,12 @@ +oneOf: + - $ref: ./TotpEnrollmentStart.yaml + - $ref: ./PasskeyEnrollmentStart.yaml +discriminator: + propertyName: type + mapping: + TOTP: ./TotpEnrollmentStart.yaml + PASSKEY: ./PasskeyEnrollmentStart.yaml +description: >- + The factor-specific material needed to complete enrollment, keyed by `type`: + a TOTP shared secret + provisioning URI, or the WebAuthn registration options + for a passkey. diff --git a/openapi/components/schemas/sca/ScaFactorList.yaml b/openapi/components/schemas/sca/ScaFactorList.yaml new file mode 100644 index 000000000..a02cac4e3 --- /dev/null +++ b/openapi/components/schemas/sca/ScaFactorList.yaml @@ -0,0 +1,10 @@ +type: object +description: The Strong Customer Authentication factors a customer has enrolled. +required: + - factors +properties: + factors: + type: array + description: The customer's enrolled SCA factors. + items: + $ref: ./ScaFactorView.yaml diff --git a/openapi/components/schemas/sca/ScaFactorView.yaml b/openapi/components/schemas/sca/ScaFactorView.yaml new file mode 100644 index 000000000..1a434f37d --- /dev/null +++ b/openapi/components/schemas/sca/ScaFactorView.yaml @@ -0,0 +1,17 @@ +type: object +description: An enrolled Strong Customer Authentication factor. +required: + - factor +properties: + factor: + $ref: ./ScaFactor.yaml + description: The kind of enrolled factor. + credentialId: + type: string + description: >- + The per-credential id, populated only for `PASSKEY` factors (the id passed + to delete a passkey). Omitted for `TOTP` and `SMS_OTP`, which have no + per-credential id. + name: + type: string + description: An optional human-readable label for this factor. diff --git a/openapi/components/schemas/sca/ScaLoginComplete.yaml b/openapi/components/schemas/sca/ScaLoginComplete.yaml new file mode 100644 index 000000000..e81240c45 --- /dev/null +++ b/openapi/components/schemas/sca/ScaLoginComplete.yaml @@ -0,0 +1,13 @@ +type: object +description: The status of a completed SCA login session. +required: + - status +properties: + status: + type: string + description: >- + The status of the login session, passed through + verbatim (Grid does not normalize it). A successful login reports + `SUCCESS`; other values indicate the login did not + complete and should be surfaced to the caller. + example: SUCCESS diff --git a/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml b/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml new file mode 100644 index 000000000..cdc1440ca --- /dev/null +++ b/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml @@ -0,0 +1,51 @@ +type: object +description: >- + Completes an SCA login by submitting the proof for the started factor. Carries + the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`, + or `passkeyAssertion` + `origin` for `PASSKEY`), plus the `factor` being + completed and, for `SMS_OTP`, the `challengeId` returned by the login start. +required: + - factor +anyOf: + - required: + - code + - required: + - passkeyAssertion + - origin +properties: + factor: + $ref: ./ScaFactor.yaml + description: The factor being completed; must match the started login. + challengeId: + type: + - string + - 'null' + description: >- + The challenge handle returned by the login start, required for `SMS_OTP` + and omitted for other factors. + code: + type: + - string + - 'null' + description: >- + The one-time code the customer received by SMS, or read from their + authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is + always `123456`. + example: '123456' + passkeyAssertion: + type: + - object + - 'null' + additionalProperties: true + description: >- + Opaque WebAuthn assertion produced by the device from the login start's + `passkeyOptions`. Required when completing a `PASSKEY` login. + origin: + type: + - string + - 'null' + description: >- + The WebAuthn origin the `passkeyAssertion` was produced against (one of + the login start's `allowedOrigins`). Required alongside `passkeyAssertion`; + omit it for the `code` path. + example: https://app.example.com diff --git a/openapi/components/schemas/sca/ScaLoginStart.yaml b/openapi/components/schemas/sca/ScaLoginStart.yaml new file mode 100644 index 000000000..b4d71b14d --- /dev/null +++ b/openapi/components/schemas/sca/ScaLoginStart.yaml @@ -0,0 +1,55 @@ +type: object +description: >- + The factor-specific material a customer needs to complete an SCA login. Each + factor surfaces only the fields it issues: `SMS_OTP` carries `challengeId` and + `expiresAt`; `TOTP` carries neither (the customer reads the code from their + authenticator app); `PASSKEY` carries the opaque WebAuthn `passkeyOptions` + with `allowedOrigins` and `relyingPartyId`. +required: + - factor +properties: + factor: + $ref: ./ScaFactor.yaml + description: The factor this login was started for. + challengeId: + type: + - string + - 'null' + description: >- + The challenge handle for an `SMS_OTP` login, threaded back on the complete + call. Present only for `SMS_OTP`. + expiresAt: + type: + - string + - 'null' + format: date-time + description: >- + Absolute UTC timestamp after which the `SMS_OTP` code expires. Present + only for `SMS_OTP`. + example: '2025-10-03T12:05:00Z' + passkeyOptions: + type: + - object + - 'null' + additionalProperties: true + description: >- + Opaque WebAuthn assertion request options. Present only for `PASSKEY`; + pass to the device's WebAuthn API to produce the assertion submitted on + the complete call. + allowedOrigins: + type: + - array + - 'null' + items: + type: string + description: >- + The origins the WebAuthn ceremony may run against. Present only for + `PASSKEY`. + example: + - https://app.example.com + relyingPartyId: + type: + - string + - 'null' + description: The WebAuthn relying-party id. Present only for `PASSKEY`. + example: app.example.com diff --git a/openapi/components/schemas/sca/ScaLoginStartRequest.yaml b/openapi/components/schemas/sca/ScaLoginStartRequest.yaml new file mode 100644 index 000000000..9974b26db --- /dev/null +++ b/openapi/components/schemas/sca/ScaLoginStartRequest.yaml @@ -0,0 +1,10 @@ +type: object +description: >- + Selects which enrolled factor to start an SCA login with. The factor must + already be enrolled (or, for `SMS_OTP`, the phone verified). +required: + - factor +properties: + factor: + $ref: ./ScaFactor.yaml + description: The factor to authenticate with. diff --git a/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml b/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml new file mode 100644 index 000000000..df6bc1a81 --- /dev/null +++ b/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml @@ -0,0 +1,26 @@ +type: object +description: >- + The shared secret returned by the TOTP enrollment start, plus the first code + the customer's authenticator app produces, submitted to confirm and finalize + the TOTP factor. +required: + - type + - secret + - code +properties: + type: + type: string + enum: + - TOTP + description: Discriminator selecting the TOTP confirm variant. + secret: + type: string + description: >- + The shared secret returned as `secret` by the TOTP enrollment start, + threaded back to bind the confirmation to that enrollment. + code: + type: string + description: >- + The current time-based one-time code from the customer's authenticator + app. In sandbox, the code is always `123456`. + example: '123456' diff --git a/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml b/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml new file mode 100644 index 000000000..b3d7e9aab --- /dev/null +++ b/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml @@ -0,0 +1,22 @@ +type: object +description: >- + The one-time recovery codes issued once a TOTP factor is enrolled. These are + shown to the customer only once; store them somewhere safe to recover access + if the authenticator device is lost. +required: + - type + - recoveryCodes +properties: + type: + type: string + enum: + - TOTP + description: Discriminator identifying this as the TOTP enrollment result. + recoveryCodes: + type: array + description: The one-time recovery codes for this TOTP factor. + items: + type: string + example: + - ABCD-EFGH-IJKL + - MNOP-QRST-UVWX diff --git a/openapi/components/schemas/sca/TotpEnrollmentStart.yaml b/openapi/components/schemas/sca/TotpEnrollmentStart.yaml new file mode 100644 index 000000000..26dffef54 --- /dev/null +++ b/openapi/components/schemas/sca/TotpEnrollmentStart.yaml @@ -0,0 +1,31 @@ +type: object +description: >- + The shared secret a customer's authenticator app needs to enroll a TOTP + factor. Returned by `POST /sca/factors` for a `TOTP` request; the + customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with + the first code their app produces. +required: + - type + - secret + - secretBase32Encoded + - totpUri +properties: + type: + type: string + enum: + - TOTP + description: Discriminator identifying this as the TOTP enrollment-start payload. + secret: + type: string + description: The raw TOTP shared secret. + secretBase32Encoded: + type: string + description: >- + The Base32-encoded shared secret, suitable for manual entry into an + authenticator app that does not scan QR codes. + totpUri: + type: string + description: >- + The `otpauth://` provisioning URI (the QR-code payload) the customer's + authenticator app scans to enroll the factor. + example: otpauth://totp/Grid:customer@example.com?secret=ABC123&issuer=Grid diff --git a/openapi/components/schemas/sca/TotpFactorEnrollRequest.yaml b/openapi/components/schemas/sca/TotpFactorEnrollRequest.yaml new file mode 100644 index 000000000..80db0c56d --- /dev/null +++ b/openapi/components/schemas/sca/TotpFactorEnrollRequest.yaml @@ -0,0 +1,11 @@ +type: object +title: TOTP Factor Enroll Request +description: Start enrolling a time-based one-time-password (TOTP) authenticator factor. +required: + - type +properties: + type: + type: string + enum: + - TOTP + description: Discriminator selecting the TOTP factor. TOTP enrollment needs no other input at start. diff --git a/openapi/components/schemas/sca/TwoFactorResetCompleteRequest.yaml b/openapi/components/schemas/sca/TwoFactorResetCompleteRequest.yaml new file mode 100644 index 000000000..46df49fba --- /dev/null +++ b/openapi/components/schemas/sca/TwoFactorResetCompleteRequest.yaml @@ -0,0 +1,23 @@ +type: object +description: >- + Optional body for completing a 2FA reset. Only needed when resetting the + `SMS_OTP` factor to a new phone number; omit the body entirely otherwise. +properties: + mobile: + type: object + description: >- + For an `SMS_OTP` reset, the new mobile number to enroll as the customer + completes the reset. Required for an `SMS_OTP` reset; ignored for `TOTP` / + `PASSKEY` resets. + required: + - countryCode + - number + properties: + countryCode: + type: string + description: The country dialing code, including the leading `+`. + example: '+1' + number: + type: string + description: The national subscriber number, without the country code. + example: '4155550123' diff --git a/openapi/components/schemas/sca/TwoFactorResetStart.yaml b/openapi/components/schemas/sca/TwoFactorResetStart.yaml new file mode 100644 index 000000000..e0183c819 --- /dev/null +++ b/openapi/components/schemas/sca/TwoFactorResetStart.yaml @@ -0,0 +1,35 @@ +type: object +description: >- + The reset handle plus the opaque liveness handles a caller relays to + the end-user device to complete the liveness check. `resetId` threads the + ceremony together (status and complete reference it). `livenessAccessToken` + and `verificationLink` are omitted when they are not returned. +required: + - resetId +properties: + resetId: + type: string + description: >- + Identifier for this reset; pass it to the status and complete endpoints. + livenessAccessToken: + type: + - string + - 'null' + description: >- + Access token for the embedded liveness/verification SDK, + bound to this reset. Omitted when one is not returned. + verificationLink: + type: + - string + - 'null' + description: >- + Hosted identity-verification page URL for completing liveness. Omitted + when one is not returned. + expiresAt: + type: + - string + - 'null' + format: date-time + description: >- + Absolute UTC timestamp at the end of the reset window. Omitted when one is not returned. + example: '2025-10-03T12:30:00Z' diff --git a/openapi/components/schemas/sca/TwoFactorResetStartRequest.yaml b/openapi/components/schemas/sca/TwoFactorResetStartRequest.yaml new file mode 100644 index 000000000..2af4a6daf --- /dev/null +++ b/openapi/components/schemas/sca/TwoFactorResetStartRequest.yaml @@ -0,0 +1,8 @@ +type: object +description: Selects which enrolled factor to reset via the liveness-gated recovery flow. +required: + - factor +properties: + factor: + $ref: ./ScaFactor.yaml + description: The enrolled factor to reset. diff --git a/openapi/components/schemas/sca/TwoFactorResetStatus.yaml b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml new file mode 100644 index 000000000..c26598b62 --- /dev/null +++ b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml @@ -0,0 +1,59 @@ +type: object +description: The status of an in-progress 2FA reset, polled until it reaches a terminal value. +required: + - status + - factor + - expiresAt +properties: + status: + type: string + enum: + - INITIATED + - PENDING_REVIEW + - LIVENESS_PASSED + - COMPLETED + - REJECTED + - EXPIRED + description: >- + The reset status. + + | Value | Terminal | Meaning | + |-------|----------|---------| + | `INITIATED` | no | Reset started; liveness not yet submitted. Keep polling. | + | `PENDING_REVIEW` | no | Liveness submitted; under review. Keep polling. | + | `LIVENESS_PASSED` | no | Liveness passed; call the complete endpoint to finish the reset. | + | `COMPLETED` | **yes** | Reset finished; the lost factor is cleared and re-enrollment can begin. | + | `REJECTED` | **yes** | Liveness failed. Stop polling; start a new reset. | + | `EXPIRED` | **yes** | The reset window closed before it completed. Stop polling; start a new reset. | + + Stop polling on any terminal value. + example: INITIATED + factor: + $ref: ./ScaFactor.yaml + description: The factor being reset. + enrollmentStatus: + type: + - string + - 'null' + enum: + - PENDING + - COMPLETED + - null + description: >- + The enrollment status of the replacement factor. `PENDING` until the + customer finishes re-enrolling, then `COMPLETED`. Null for an `SMS_OTP` + reset, where completing the reset enrolls the new number directly with no + separate enrollment step. + example: PENDING + expiresAt: + type: string + format: date-time + description: When the reset window closes. Poll no longer than this. + example: '2025-10-03T12:15:00Z' + completedAt: + type: + - string + - 'null' + format: date-time + description: When the reset completed; null until then. + example: null diff --git a/openapi/components/schemas/transfers/TransferOutRequest.yaml b/openapi/components/schemas/transfers/TransferOutRequest.yaml index 865360b34..1678b4070 100644 --- a/openapi/components/schemas/transfers/TransferOutRequest.yaml +++ b/openapi/components/schemas/transfers/TransferOutRequest.yaml @@ -26,11 +26,3 @@ properties: remittanceInformation field, and for wires it populates the OBI (Originator to Beneficiary Information) / beneficiary information. example: '12345' - scaFactor: - $ref: ../sca/ScaFactor.yaml - description: >- - Optional preferred factor for the Strong Customer Authentication challenge - this call issues. Only relevant for customers in a region where SCA is required - (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge - are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required - dynamic linking and is rejected here. Omit to default to `SMS_OTP`. diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index a4865267f..7254a8e98 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -153,6 +153,32 @@ paths: $ref: paths/customers/customers_{customerId}_verify-phone.yaml /customers/{customerId}/verify-phone/confirm: $ref: paths/customers/customers_{customerId}_verify-phone_confirm.yaml + /sca/factors: + $ref: paths/sca/sca_factors.yaml + /sca/factors/confirm: + $ref: paths/sca/sca_factors_confirm.yaml + /sca/factors/{credentialId}: + $ref: paths/sca/sca_factors_{credentialId}.yaml + /sca/login/start: + $ref: paths/sca/sca_login_start.yaml + /sca/login/complete: + $ref: paths/sca/sca_login_complete.yaml + /sca/record-event: + $ref: paths/sca/sca_record-event.yaml + /sca/factors/reset: + $ref: paths/sca/sca_factors_reset.yaml + /sca/factors/reset/{resetId}: + $ref: paths/sca/sca_factors_reset_{resetId}.yaml + /sca/factors/reset/{resetId}/complete: + $ref: paths/sca/sca_factors_reset_{resetId}_complete.yaml + /customers/external-accounts/{externalAccountId}/trust: + $ref: paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml + /customers/external-accounts/{externalAccountId}/trust/confirm: + $ref: paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml + /customers/external-accounts/{externalAccountId}/untrust: + $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 /customers/internal-accounts: $ref: paths/customers/customers_internal_accounts.yaml /platform/internal-accounts: diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml new file mode 100644 index 000000000..52853b191 --- /dev/null +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml @@ -0,0 +1,58 @@ +parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being trusted. + required: true + schema: + type: string +post: + summary: Start trusting a beneficiary + description: | + Begin trusting (whitelisting) an external account so future sends to it can + skip the per-transaction SCA ceremony. Returns the `scaChallenge` to satisfy + (when one is issued). Complete with + `POST /customers/external-accounts/{externalAccountId}/trust/confirm`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startBeneficiaryTrust + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '200': + description: Beneficiary trust started; the SCA challenge (if any) is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/BeneficiaryTrustStart.yaml + '400': + description: Invalid request + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml new file mode 100644 index 000000000..d69d2e888 --- /dev/null +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml @@ -0,0 +1,66 @@ +parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being trusted. + required: true + schema: + type: string +post: + summary: Confirm trusting a beneficiary + description: | + Finalize trusting a beneficiary (identified by the `externalAccountId` in the + path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when + one was issued. Returns `trusted: true`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryTrust + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml + responses: + '200': + description: Beneficiary trusted. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/BeneficiaryTrustConfirm.yaml + '400': + description: Invalid or expired proof + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml new file mode 100644 index 000000000..85e17b1e3 --- /dev/null +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml @@ -0,0 +1,59 @@ +parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being untrusted. + required: true + schema: + type: string +post: + summary: Start untrusting a beneficiary + description: | + Begin untrusting (removing the trusted mark from) an external account, so + future sends to it are dynamically linked and require the per-transaction SCA + ceremony again. Returns the `scaChallenge` to satisfy (when one is issued). + Complete with + `POST /customers/external-accounts/{externalAccountId}/untrust/confirm`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startBeneficiaryUntrust + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '200': + description: Beneficiary untrust started; the SCA challenge (if any) is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/BeneficiaryTrustStart.yaml + '400': + description: Invalid request + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml new file mode 100644 index 000000000..ec5d38a64 --- /dev/null +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml @@ -0,0 +1,66 @@ +parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being untrusted. + required: true + schema: + type: string +post: + summary: Confirm untrusting a beneficiary + description: | + Finalize untrusting a beneficiary (identified by the `externalAccountId` in + the path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when + one was issued. Returns `trusted: false`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryUntrust + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml + responses: + '200': + description: Beneficiary untrusted. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/BeneficiaryTrustConfirm.yaml + '400': + description: Invalid or expired proof + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 diff --git a/openapi/paths/sca/sca_factors.yaml b/openapi/paths/sca/sca_factors.yaml new file mode 100644 index 000000000..5d6da61a6 --- /dev/null +++ b/openapi/paths/sca/sca_factors.yaml @@ -0,0 +1,121 @@ +parameters: + - name: customerId + in: query + description: The unique identifier of the customer whose factors are listed or enrolled. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +get: + summary: List enrolled SCA factors + description: | + List the Strong Customer Authentication factors the customer has enrolled. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: listScaFactors + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '200': + description: The customer's enrolled SCA factors. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaFactorList.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 +post: + summary: Start SCA factor enrollment + description: | + Begin enrolling an SCA factor for the customer. Enrollment covers the + explicit, opt-in factors a customer chooses to add — the request body's + `type` selects `TOTP` or `PASSKEY`. Returns the factor-specific material + needed to finish via `POST /sca/factors/confirm`. + + `SMS_OTP` is implicit and is not enrolled through this endpoint. Every + customer in an SCA-regulated region has a verified phone number from + customer creation (via the Contact Verification flows — + `POST /customers/{customerId}/verify-phone` and `.../verify-phone/confirm`), + so SMS is always available as a factor with no extra setup and appears + among the customer's enrolled factors in `GET /sca/factors`. + + A customer may have **only one passkey**. Starting a passkey enrollment when + one is already enrolled returns `409` (`PASSKEY_ALREADY_ENROLLED`) — delete it + via `DELETE /sca/factors/{credentialId}` first. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startScaFactorEnrollment + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaFactorEnrollRequestOneOf.yaml + responses: + '200': + description: Enrollment started; the factor-specific completion material is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaFactorEnrollStartOneOf.yaml + '400': + description: Invalid request + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + SCA is not required for this customer (`CONFLICT`), or a passkey + enrollment was requested while one is already enrolled + (`PASSKEY_ALREADY_ENROLLED`) — only one passkey per customer is supported. + 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 diff --git a/openapi/paths/sca/sca_factors_confirm.yaml b/openapi/paths/sca/sca_factors_confirm.yaml new file mode 100644 index 000000000..370cc219f --- /dev/null +++ b/openapi/paths/sca/sca_factors_confirm.yaml @@ -0,0 +1,72 @@ +parameters: + - name: customerId + in: query + description: The unique identifier of the customer confirming a factor enrollment. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Confirm SCA factor enrollment + description: | + Finalize the factor enrollment started by `POST /sca/factors`. The request + body is discriminated by `type`: for `TOTP`, submit the shared `secret` from + the start call plus the first `code`; for `PASSKEY`, submit the WebAuthn + `credential` the device produced plus the `origin` it was produced against. + The threaded secret/credential binds the confirmation to its enrollment, so + no separate id is needed. + + A TOTP confirmation returns one-time recovery codes (shown once); a passkey + confirmation returns the enrolled factor. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the TOTP code is always `123456`. + operationId: confirmScaFactorEnrollment + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml + responses: + '200': + description: Factor enrolled; the factor-specific result is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml + '400': + description: Invalid or incorrect confirmation proof + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 diff --git a/openapi/paths/sca/sca_factors_reset.yaml b/openapi/paths/sca/sca_factors_reset.yaml new file mode 100644 index 000000000..d7fa24870 --- /dev/null +++ b/openapi/paths/sca/sca_factors_reset.yaml @@ -0,0 +1,75 @@ +parameters: + - name: customerId + in: query + description: The unique identifier of the customer resetting a factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Start a 2FA reset + description: | + Begin recovering a lost enrolled factor via a liveness-gated, poll-based + flow. Opens the liveness check and returns a `resetId` plus the + opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end + user completes it with. Poll + `GET /sca/factors/reset/{resetId}` until liveness + passes, then call the complete endpoint. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startTwoFactorReset + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/TwoFactorResetStartRequest.yaml + responses: + '201': + description: Reset initiated; the reset handle and liveness material are returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/TwoFactorResetStart.yaml + '400': + description: Invalid or unknown factor + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '429': + description: >- + Too many reset attempts. Reset initiation is rate-limited to 5 per 24 + hours per customer; retry after the window indicated by `Retry-After`. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error429.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/sca/sca_factors_reset_{resetId}.yaml b/openapi/paths/sca/sca_factors_reset_{resetId}.yaml new file mode 100644 index 000000000..dcf436481 --- /dev/null +++ b/openapi/paths/sca/sca_factors_reset_{resetId}.yaml @@ -0,0 +1,56 @@ +parameters: + - name: customerId + in: query + description: The unique identifier of the customer whose reset status is polled. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: resetId + in: path + description: The reset handle returned by the start call. + required: true + schema: + type: string +get: + summary: Get 2FA reset status + description: | + Poll the status of an in-progress 2FA reset until it reaches the liveness-passed value, after which the reset can be completed. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: getTwoFactorResetStatus + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '200': + description: The current reset status. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/TwoFactorResetStatus.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer or reset not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 diff --git a/openapi/paths/sca/sca_factors_reset_{resetId}_complete.yaml b/openapi/paths/sca/sca_factors_reset_{resetId}_complete.yaml new file mode 100644 index 000000000..2d454df13 --- /dev/null +++ b/openapi/paths/sca/sca_factors_reset_{resetId}_complete.yaml @@ -0,0 +1,68 @@ +parameters: + - name: customerId + in: query + description: The unique identifier of the customer completing the reset. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: resetId + in: path + description: The reset handle returned by the start call. + required: true + schema: + type: string +post: + summary: Complete a 2FA reset + description: | + Complete a 2FA reset once liveness has passed, clearing the lost factor so + the customer can re-enroll. + + For an `SMS_OTP` reset, supply the new `mobile` number in the body — completing + the reset enrolls it as the customer's number. Other factors need no body. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: completeTwoFactorReset + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: false + content: + application/json: + schema: + $ref: ../../components/schemas/sca/TwoFactorResetCompleteRequest.yaml + responses: + '204': + description: Reset completed; no content is returned. + '400': + description: Reset not ready (liveness not yet passed) + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer or reset not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 diff --git a/openapi/paths/sca/sca_factors_{credentialId}.yaml b/openapi/paths/sca/sca_factors_{credentialId}.yaml new file mode 100644 index 000000000..4e651d44b --- /dev/null +++ b/openapi/paths/sca/sca_factors_{credentialId}.yaml @@ -0,0 +1,53 @@ +parameters: + - name: customerId + in: query + description: The unique identifier of the customer whose factor is being deleted. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: credentialId + in: path + description: The credential id of the enrolled factor to delete (from the factor's `credentialId`). + required: true + schema: + type: string +delete: + summary: Delete an enrolled SCA factor + description: | + Delete an enrolled SCA factor by its credential id. Today only `PASSKEY` + factors carry a `credentialId` and are deletable this way. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: deleteScaFactor + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '204': + description: Factor deleted; no content is returned. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer or factor not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 diff --git a/openapi/paths/sca/sca_login_complete.yaml b/openapi/paths/sca/sca_login_complete.yaml new file mode 100644 index 000000000..8f85304f2 --- /dev/null +++ b/openapi/paths/sca/sca_login_complete.yaml @@ -0,0 +1,75 @@ +parameters: + - name: customerId + in: query + description: The unique identifier of the customer completing an SCA login. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Complete an SCA login + description: | + Finalize an SCA login by submitting the proof for the started factor + (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for + `PASSKEY`), echoing the `challengeId` for `SMS_OTP`. Returns the + reported session status. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: completeScaLogin + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaLoginCompleteRequest.yaml + responses: + '200': + description: SCA login completed; the session status is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaLoginComplete.yaml + '400': + description: Invalid or expired proof + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '423': + description: >- + The customer's login is locked (or suspended) after too many failed + attempts. `details.lockedUntil` says when they may retry. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error423.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/sca/sca_login_start.yaml b/openapi/paths/sca/sca_login_start.yaml new file mode 100644 index 000000000..1329c138c --- /dev/null +++ b/openapi/paths/sca/sca_login_start.yaml @@ -0,0 +1,68 @@ +parameters: + - name: customerId + in: query + description: The unique identifier of the customer starting an SCA login. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Start an SCA login + description: | + Begin an SCA login for the customer with the chosen factor, opening the + end-user SCA session (an exemption gating read / account access beyond the + per-transaction window). Returns factor-specific material: `SMS_OTP` + dispatches a code and returns a `challengeId` + `expiresAt`; `TOTP` returns + only the factor (the customer reads the code from their app); `PASSKEY` + returns WebAuthn `passkeyOptions`. Complete with + `POST /sca/login/complete`. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: startScaLogin + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaLoginStartRequest.yaml + responses: + '200': + description: SCA login started; factor-specific material is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/ScaLoginStart.yaml + '400': + description: Invalid or unknown factor + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + 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 diff --git a/openapi/paths/sca/sca_record-event.yaml b/openapi/paths/sca/sca_record-event.yaml new file mode 100644 index 000000000..a2dbbabd0 --- /dev/null +++ b/openapi/paths/sca/sca_record-event.yaml @@ -0,0 +1,73 @@ +parameters: + - name: customerId + in: query + description: The unique identifier of the customer the security event is recorded for. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Record a security event + description: | + Record a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view), to + feed adaptive-authentication signals. + + This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`. + operationId: recordSecurityEvent + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/RecordSecurityEventRequest.yaml + responses: + '200': + description: >- + Event recorded; the customer's resulting login-security state is + returned (including any lockout). + content: + application/json: + schema: + $ref: ../../components/schemas/sca/RecordSecurityEventResponse.yaml + '400': + description: Invalid event type + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: SCA is not required for this customer. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '423': + description: >- + The customer's login is locked (or suspended) after too many failed + attempts. `details.lockedUntil` says when they may retry. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error423.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/transfers/transfer_out.yaml b/openapi/paths/transfers/transfer_out.yaml index 85831695f..47a032663 100644 --- a/openapi/paths/transfers/transfer_out.yaml +++ b/openapi/paths/transfers/transfer_out.yaml @@ -38,17 +38,6 @@ post: '201': description: > Transfer-out request created successfully. - - - For customers outside SCA-regulated regions the transaction proceeds as - usual. - - - **Strong Customer Authentication (EU):** per-transaction SCA is authorized - only on the quote resource (`POST /quotes/{quoteId}/authorize`), and - `transfer-out` has no associated quote. `transfer-out` is being - deprecated, so SCA-gated EU debits are not offered on this endpoint — use - the quote + `execute` flow instead. content: application/json: schema: