From b7d0f0514717d3d343b4978f84684b65d0e9508a Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 17 Jun 2026 14:38:55 -0700 Subject: [PATCH 01/10] feat(sca): add SCA management surface (enrollment, login, trust, reset) Stacks on the per-transaction SCA spec (#558) and adds the remaining Strong Customer Authentication surfaces an EU (Striga) partner needs, mirroring the sparkcore handlers' exact routes + JSON shapes. All endpoints are EU-only (409 for providers that don't require SCA). - Factor enrollment: TOTP start/confirm, passkey register start/confirm, list factors, delete passkey. - SCA login / 180-day session: login start/complete, plus record-event. - Beneficiary trust: trust start (issues the SCA challenge + whitelistedId), trust/confirm and untrust/confirm (the whitelisting exemption that lets recurring payees skip per-transaction SCA). - 2FA reset: initiate (201) -> poll status -> complete. Reuses ScaFactor/ScaChallenge/ScaAuthorization from #558. Confirm/login bodies carry the code|passkeyAssertion+origin proof; beneficiary confirm threads the whitelistedId back so confirm never re-whitelists. Co-Authored-By: Claude Opus 4.8 --- mintlify/openapi.yaml | 9514 ++++++++++------- openapi.yaml | 9514 ++++++++++------- .../schemas/sca/BeneficiaryTrustConfirm.yaml | 12 + .../sca/BeneficiaryTrustConfirmRequest.yaml | 47 + .../schemas/sca/BeneficiaryTrustStart.yaml | 21 + .../sca/PasskeyEnrollmentConfirmRequest.yaml | 20 + .../sca/PasskeyEnrollmentConfirmResponse.yaml | 7 + .../schemas/sca/PasskeyEnrollmentStart.yaml | 31 + .../sca/RecordSecurityEventRequest.yaml | 12 + .../components/schemas/sca/ScaFactorList.yaml | 10 + .../components/schemas/sca/ScaFactorView.yaml | 21 + .../schemas/sca/ScaLoginComplete.yaml | 9 + .../schemas/sca/ScaLoginCompleteRequest.yaml | 45 + .../components/schemas/sca/ScaLoginStart.yaml | 55 + .../schemas/sca/ScaLoginStartRequest.yaml | 10 + .../sca/TotpEnrollmentConfirmRequest.yaml | 20 + .../sca/TotpEnrollmentConfirmResponse.yaml | 16 + .../schemas/sca/TotpEnrollmentStart.yaml | 25 + .../schemas/sca/TwoFactorResetStart.yaml | 36 + .../sca/TwoFactorResetStartRequest.yaml | 8 + .../schemas/sca/TwoFactorResetStatus.yaml | 11 + openapi/openapi.yaml | 30 + ...al-accounts_{externalAccountId}_trust.yaml | 68 + ...nts_{externalAccountId}_trust_confirm.yaml | 75 + ...s_{externalAccountId}_untrust_confirm.yaml | 75 + .../customers_{customerId}_sca_factors.yaml | 52 + ...mers_{customerId}_sca_factors_passkey.yaml | 61 + ...stomerId}_sca_factors_passkey_confirm.yaml | 66 + ...d}_sca_factors_passkey_{credentialId}.yaml | 54 + ...tomers_{customerId}_sca_factors_reset.yaml | 69 + ...stomerId}_sca_factors_reset_{resetId}.yaml | 59 + ..._sca_factors_reset_{resetId}_complete.yaml | 61 + ...stomers_{customerId}_sca_factors_totp.yaml | 61 + ...{customerId}_sca_factors_totp_confirm.yaml | 68 + ...omers_{customerId}_sca_login_complete.yaml | 69 + ...ustomers_{customerId}_sca_login_start.yaml | 70 + ...stomers_{customerId}_sca_record-event.yaml | 62 + 37 files changed, 12306 insertions(+), 8138 deletions(-) create mode 100644 openapi/components/schemas/sca/BeneficiaryTrustConfirm.yaml create mode 100644 openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml create mode 100644 openapi/components/schemas/sca/BeneficiaryTrustStart.yaml create mode 100644 openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml create mode 100644 openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml create mode 100644 openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml create mode 100644 openapi/components/schemas/sca/RecordSecurityEventRequest.yaml create mode 100644 openapi/components/schemas/sca/ScaFactorList.yaml create mode 100644 openapi/components/schemas/sca/ScaFactorView.yaml create mode 100644 openapi/components/schemas/sca/ScaLoginComplete.yaml create mode 100644 openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml create mode 100644 openapi/components/schemas/sca/ScaLoginStart.yaml create mode 100644 openapi/components/schemas/sca/ScaLoginStartRequest.yaml create mode 100644 openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml create mode 100644 openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml create mode 100644 openapi/components/schemas/sca/TotpEnrollmentStart.yaml create mode 100644 openapi/components/schemas/sca/TwoFactorResetStart.yaml create mode 100644 openapi/components/schemas/sca/TwoFactorResetStartRequest.yaml create mode 100644 openapi/components/schemas/sca/TwoFactorResetStatus.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_login_start.yaml create mode 100644 openapi/paths/customers/customers_{customerId}_sca_record-event.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 1e57d7e52..9abcb3e60 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -1127,180 +1127,93 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/internal-accounts: + /customers/{customerId}/sca/factors: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer whose enrolled factors are listed. + 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 whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: The customer's payment provider does not require SCA. 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 + /customers/{customerId}/sca/factors/totp: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer enrolling a TOTP factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Start TOTP 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 a time-based one-time-password (TOTP) authenticator factor + for the customer. Returns the shared secret and an `otpauth://` provisioning + URI; the customer scans it into an authenticator app and confirms with the + first code via `POST /customers/{customerId}/sca/factors/totp/confirm`. - External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform. - operationId: listCustomerExternalAccounts + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startTotpFactorEnrollment 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 responses: '200': - description: Successful operation + description: TOTP enrollment started; the shared secret is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' + $ref: '#/components/schemas/TotpEnrollmentStart' '400': - description: Bad request - Invalid parameters + description: Invalid request content: application/json: schema: @@ -1311,18 +1224,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/{customerId}/sca/factors/totp/confirm: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer confirming a TOTP factor. + 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 TOTP factor enrollment + description: | + Finalize TOTP factor enrollment by submitting the shared secret from the + start call and the first code the customer's authenticator app produces. + Returns one-time recovery codes shown to the customer only once. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the code is always `123456`. + operationId: confirmTotpFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1330,48 +1273,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/TotpEnrollmentConfirmRequest' responses: - '201': - description: External account created successfully + '200': + description: TOTP factor enrolled; recovery codes are returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/TotpEnrollmentConfirmResponse' '400': - description: Bad request + description: Invalid or incorrect confirmation code content: application/json: schema: @@ -1382,8 +1293,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: The customer's payment provider does not require SCA. content: application/json: schema: @@ -1394,29 +1311,44 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/external-accounts/{externalAccountId}: + /customers/{customerId}/sca/factors/passkey: parameters: - - name: externalAccountId + - name: customerId in: path - description: System-generated unique external account identifier + description: The unique identifier of the customer enrolling a passkey factor. 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 + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Start passkey factor enrollment + description: | + Begin enrolling a WebAuthn passkey factor for the customer. Returns opaque + WebAuthn registration `options`; pass them to the device's WebAuthn API and + submit the resulting credential via + `POST /customers/{customerId}/sca/factors/passkey/confirm`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startPasskeyFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] responses: '200': - description: Successful operation + description: Passkey enrollment started; WebAuthn registration options are returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/PasskeyEnrollmentStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1424,28 +1356,66 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error 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 + /customers/{customerId}/sca/factors/passkey/confirm: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer confirming a passkey factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Confirm passkey factor enrollment + description: | + Finalize passkey factor enrollment by submitting the WebAuthn credential the + device produced for the registration challenge, along with the origin it was + produced against. Returns the enrolled factor. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: confirmPasskeyFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PasskeyEnrollmentConfirmRequest' responses: - '204': - description: External account deleted successfully + '200': + description: Passkey factor enrolled; the enrolled factor is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse' + '400': + description: Invalid credential or origin + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1453,82 +1423,104 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $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 + /customers/{customerId}/sca/factors/passkey/{credentialId}: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer whose passkey 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 passkey to delete (from the enrolled factor's `credentialId`). + required: true + schema: + type: string + delete: + summary: Delete an enrolled passkey factor description: | - Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer. + Delete an enrolled WebAuthn passkey factor by its credential id. - 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 whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: deletePasskeyFactor 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 responses: - '200': - description: Successful operation + '204': + description: Passkey deleted; no content is returned. + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' - '400': - description: Bad request - Invalid parameters + $ref: '#/components/schemas/Error401' + '404': + description: Customer or passkey not found content: application/json: schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. 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/{customerId}/sca/login/start: + parameters: + - name: customerId + in: path + 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: Add a new platform external account - description: Register a new external bank account for the platform. - operationId: createPlatformExternalAccount + 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 /customers/{customerId}/sca/login/complete`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startScaLogin tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1536,46 +1528,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/ScaLoginStartRequest' responses: - '201': - description: External account created successfully + '200': + description: SCA login started; factor-specific material is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/ScaLoginStart' '400': - description: Bad request + description: Invalid or unknown factor content: application/json: schema: @@ -1586,8 +1548,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: The customer's payment provider does not require SCA. content: application/json: schema: @@ -1598,29 +1566,52 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts/{externalAccountId}: + /customers/{customerId}/sca/login/complete: parameters: - - name: externalAccountId + - name: customerId in: path - description: System-generated unique external account identifier + description: The unique identifier of the customer completing an SCA login. 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: 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 + provider-reported session status. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: completeScaLogin tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaLoginCompleteRequest' responses: '200': - description: Successful operation + description: SCA login completed; the session status is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/ScaLoginComplete' + '400': + description: Invalid or expired proof + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1628,28 +1619,62 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '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 + /customers/{customerId}/sca/record-event: + parameters: + - name: customerId + in: path + 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 the + underlying provider's risk engine (e.g. a sign-in, a sensitive view), to + feed adaptive-authentication signals. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: '204': - description: External account deleted successfully + description: Event recorded; no content is returned. + '400': + description: Invalid event type + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1657,25 +1682,48 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /beneficial-owners: + /customers/{customerId}/sca/factors/reset: + parameters: + - name: customerId + in: path + 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 provider's liveness check and returns a `resetId` plus the + opaque liveness handles (`sumsubAccessToken` / `verificationLink`) the end + user completes it with. Poll + `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness + passes, then call the complete endpoint. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startTwoFactorReset tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1683,16 +1731,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,52 +1757,112 @@ paths: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/{customerId}/sca/factors/reset/{resetId}: + parameters: + - name: customerId + in: path + 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 provider's + liveness-passed value, after which the reset can be completed. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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 + description: The current reset status. content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwnerListResponse' + $ref: '#/components/schemas/TwoFactorResetStatus' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Customer or reset not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /customers/{customerId}/sca/factors/reset/{resetId}/complete: + parameters: + - name: customerId + in: path + 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. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: completeTwoFactorReset + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '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: @@ -1765,35 +1873,69 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer or reset not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '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 + /customers/{customerId}/external-accounts/{externalAccountId}/trust: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer who owns the external account. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - 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. Creates the whitelist entry and + returns a `whitelistedId` handle plus, when the provider issues one, the + `scaChallenge` to satisfy. Complete with + `POST /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startBeneficiaryTrust tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: beneficialOwnerId - in: path - description: Beneficial owner ID - required: true - schema: - type: string responses: '200': - description: Successful operation + description: Beneficiary trust started; the whitelist handle and any challenge are returned. content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwner' + $ref: '#/components/schemas/BeneficiaryTrustStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1801,47 +1943,71 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Beneficial owner not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $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 + /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer who owns the external account. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - 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 by submitting the `whitelistedId` from the + trust start and 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 whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryTrust 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 content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwnerUpdateRequest' + $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest' responses: '200': - description: Beneficial owner updated successfully + description: Beneficiary trusted. content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwner' + $ref: '#/components/schemas/BeneficiaryTrustConfirm' '400': - description: Bad request - Invalid parameters + description: Invalid or expired proof content: application/json: schema: @@ -1853,40 +2019,71 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Beneficial owner not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /documents: + /customers/{customerId}/external-accounts/{externalAccountId}/untrust/confirm: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer who owns the external account. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being untrusted. + required: true + schema: + type: string post: - summary: Upload a document + summary: Confirm untrusting 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. + Finalize untrusting a beneficiary by submitting the `whitelistedId` from the + trust start and the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when + one was issued. Returns `trusted: false`. - Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB. - operationId: uploadDocument + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryUntrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] requestBody: - $ref: '#/components/requestBodies/DocumentUploadRequestBody' + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest' responses: - '201': - description: Document uploaded successfully + '200': + 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: @@ -1898,33 +2095,56 @@ 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: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/internal-accounts: get: - summary: List documents + summary: List Customer internal accounts description: | - Retrieve a list of documents with optional filtering by document holder. - operationId: listDocuments + 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: documentHolder + - name: currency in: query - description: Filter by document holder ID (Customer or BeneficialOwner) + 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) @@ -1946,7 +2166,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DocumentListResponse' + $ref: '#/components/schemas/InternalAccountListResponse' '400': description: Bad request - Invalid parameters content: @@ -1965,237 +2185,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /documents/{documentId}: + /platform/internal-accounts: get: - summary: Get a document by ID - description: Retrieve details and metadata of a specific document by ID. - operationId: getDocument + 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: - - 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 - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Document' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Document not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - put: - summary: Replace a document - 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 - tags: - - Documents - security: - - BasicAuth: [] - parameters: - - name: documentId - in: path - description: Document ID - required: true - schema: - type: string - requestBody: - $ref: '#/components/requestBodies/DocumentReplaceRequestBody' - responses: - '200': - description: Document replaced successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Document' - '400': - description: Bad request - Invalid file type, size, or parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Document 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 a document - description: | - Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable. - operationId: deleteDocument - tags: - - Documents - security: - - BasicAuth: [] - parameters: - - name: documentId - in: path - description: Document ID - required: true + - 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: - type: string - responses: - '204': - description: Document deleted successfully - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Document not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Document cannot be deleted (already submitted for verification) - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /verifications: - post: - summary: Submit customer for verification - 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 - - **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: - - KYC/KYB Verifications - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/VerificationRequest' + $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 +2229,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 +2283,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/VerificationListResponse' + $ref: '#/components/schemas/ExternalAccountListResponse' '400': description: Bad request - Invalid parameters content: @@ -2282,94 +2302,106 @@ 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 + requestBody: + required: true + 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 responses: - '200': - description: Successful operation + '201': + description: External account created successfully content: application/json: schema: - $ref: '#/components/schemas/Verification' + $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' - '404': - description: Verification 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-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 + /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/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: - '201': - description: Transfer-in request created successfully - 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: @@ -2377,7 +2409,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Customer or account not found + description: External account not found content: application/json: schema: @@ -2388,60 +2420,17 @@ paths: 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 + delete: + summary: Delete customer external account by ID + description: Delete a customer external account by its system-generated ID + operationId: deleteCustomerExternalAccountById 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 - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + '204': + description: External account deleted successfully '401': description: Unauthorized content: @@ -2449,7 +2438,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Customer or account not found + description: External account not found content: application/json: schema: @@ -2460,45 +2449,49 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /receiver/uma/{receiverUmaAddress}: + /platform/external-accounts: get: - summary: Look up an UMA address for payment + summary: List platform external accounts 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 + 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: receiverUmaAddress - in: path - description: UMA address of the intended recipient - required: true + - name: currency + in: query + description: Filter by currency code + required: false schema: type: string - - 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/ReceiverUmaLookupResponse' + $ref: '#/components/schemas/ExternalAccountListResponse' '400': - description: Bad request - Missing or invalid parameters + description: Bad request - Invalid parameters content: application/json: schema: @@ -2509,70 +2502,65 @@ paths: application/json: 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`). - 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}: - get: - summary: Look up an external account for payment - 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 + post: + summary: Add a new platform external account + description: Register a new external bank account for the platform. + operationId: createPlatformExternalAccount tags: - - Cross-Currency Transfers + - External Accounts security: - BasicAuth: [] - parameters: - - name: accountId - in: path - description: System-generated ID of the external account - required: true - schema: - type: string - 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 + 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: - '200': - description: Successful lookup + '201': + description: External account created successfully content: application/json: schema: - $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse' + $ref: '#/components/schemas/ExternalAccount' '400': - description: Bad request - Missing or invalid parameters + description: Bad request content: application/json: schema: @@ -2583,56 +2571,70 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: External account not found + '409': + description: Conflict - External account already exists content: application/json: schema: - $ref: '#/components/schemas/Error404' - '412': - description: Counterparty doesn't support UMA version + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue + $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: [] + responses: + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error424' + $ref: '#/components/schemas/ExternalAccount' + '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' - /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 + delete: + summary: Delete platform external account by ID + description: Delete a platform external account by its system-generated ID + operationId: deletePlatformExternalAccountById tags: - - Cross-Currency Transfers + - 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 - content: - application/json: - schema: - $ref: '#/components/schemas/Quote' + '204': + description: External account deleted successfully '401': description: Unauthorized content: @@ -2640,7 +2642,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Quote not found + description: External account not found content: application/json: schema: @@ -2651,132 +2653,31 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes: + /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 +2688,145 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '412': - description: Counterparty doesn't support UMA version - content: - application/json: - schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue + '404': + description: Customer not found 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 +2838,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 + Retrieve a list of documents with optional filtering by document holder. + operationId: listDocuments tags: - - Transactions + - 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 +2925,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 +2950,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 +2980,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: @@ -3218,38 +2991,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 +3029,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: @@ -3272,43 +3040,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 +3066,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 +3083,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 + description: | + Verification status returned. Check `verificationStatus` and `errors` to determine next steps. content: application/json: schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: + $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/Error401' - '404': - description: Transaction not found + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized 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/Error401' + '404': + description: Customer 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' - /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 +3261,168 @@ 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. + + 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/UmaInvitation' + $ref: '#/components/schemas/TransactionOneOf' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: @@ -3658,36 +3433,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 +3495,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 +3569,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 +3636,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 + 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: - - Sandbox + - 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 +3772,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 +3948,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 +4127,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 +4162,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 +4184,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiToken' + $ref: '#/components/schemas/TransactionOneOf' '401': description: Unauthorized content: @@ -4238,7 +4192,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Token not found + description: Transaction not found content: application/json: schema: @@ -4249,19 +4203,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 +4246,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Token not found + description: Transaction not found content: application/json: schema: @@ -4284,368 +4257,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 +4435,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 +4469,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. + description: CSV upload accepted for processing 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. - 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 +4643,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': - description: Internal service error - content: + '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 +4735,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 +4752,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 +4814,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 +4845,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 +4904,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 +4993,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 +5082,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 +5109,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 +5174,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AgentListResponse' + $ref: '#/components/schemas/TokenListResponse' '400': description: Bad request - Invalid parameters content: @@ -5641,153 +5193,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 +5389,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 +5459,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 +5476,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 +5647,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 +5713,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 + $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/Error404' - '500': - description: Internal service error + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized 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/Error401' + '404': + description: Authentication credential not found content: application/json: schema: - $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse' - '400': - description: Bad request (e.g., code already redeemed or expired) - content: - application/json: + $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: - $ref: '#/components/schemas/Error400' - '404': - description: Device code not found + 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 +6075,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 +6145,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 +6250,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 +6354,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 +6433,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Action not found + description: Delegated key not found content: application/json: schema: @@ -6629,69 +6444,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 +6490,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 +6525,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 +6607,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 +6626,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 +6678,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 +6713,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 +6772,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 +6829,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 +6846,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Agent not found content: application/json: schema: @@ -7035,18 +6857,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 +6879,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 +6899,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 +6947,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 +7010,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 +7094,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 +7136,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 +7178,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 +7283,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 +7331,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 +7377,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 +7442,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 +7521,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 +7559,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 +7572,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 +7603,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Stablecoin provider account link not found + description: Action not found content: application/json: schema: @@ -7801,290 +7614,1462 @@ 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. + 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: + - Agent Operations + security: + - 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: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountListResponse' + '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/external-accounts: + get: + summary: List agent external accounts + 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 + tags: + - Agent Operations + security: + - 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: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccountListResponse' + '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' + post: + summary: Add an external account + 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 + 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 + 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' + /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: | + 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: + - Agent Operations + security: + - AgentAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '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' + 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: Issue a card + 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 + tags: + - Cards + security: + - BasicAuth: [] + requestBody: + required: true + 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 + 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 @@ -10688,22 +11673,202 @@ components: BusinessCustomerCreateRequest: title: Business Customer Create Request allOf: - - $ref: '#/components/schemas/CustomerCreateRequest' + - $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 | + | 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 + 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: + - message + - status + - code + properties: + status: + type: integer + enum: + - 410 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | CUSTOMER_DELETED | Customer has been permanently deleted | + enum: + - CUSTOMER_DELETED + message: + type: string + 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 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 + 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' - - type: object - properties: - businessInfo: - $ref: '#/components/schemas/BusinessInfo' - CustomerCreateRequestOneOf: + CustomerUpdateRequestOneOf: oneOf: - - $ref: '#/components/schemas/IndividualCustomerCreateRequest' - - $ref: '#/components/schemas/BusinessCustomerCreateRequest' + - $ref: '#/components/schemas/IndividualCustomerUpdateRequest' + - $ref: '#/components/schemas/BusinessCustomerUpdateRequest' discriminator: propertyName: customerType mapping: - INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest' - BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest' - Error409: + INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' + BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' + SignedRequestChallenge: + title: Signed Request Challenge + type: object + 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. + properties: + payloadToSign: + 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: + 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 + expiresAt: + type: string + 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: type: object required: - message @@ -10713,24 +11878,24 @@ components: status: type: integer enum: - - 409 + - 424 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 | - | 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 | + | 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: - - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL - - UMA_ADDRESS_EXISTS - - EMAIL_OTP_EMAIL_ALREADY_EXISTS - - EMAIL_OTP_CREDENTIAL_SET_CHANGED - - CONFLICT + - 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 @@ -10738,234 +11903,445 @@ components: type: object description: Additional error details additionalProperties: true - Error404: + 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: + - kycUrl + - expiresAt + - provider + properties: + kycUrl: + 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 + 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: - - 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: + 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: + - factor + properties: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The kind of enrolled factor. + credentialId: + type: + - string + - 'null' + 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 + - 'null' + 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: + $ref: '#/components/schemas/ScaFactorView' + TotpEnrollmentStart: + type: object + description: The shared secret a customer's authenticator app needs to enroll a TOTP factor. Returned by `POST /customers/{customerId}/sca/factors/totp`; the customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with the first code their app produces. + required: + - secret + - secretBase32Encoded + - totpUri + properties: + secret: type: string - description: Error message - details: + 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 + 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: + - secret + - code + properties: + 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' + 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: + - recoveryCodes + properties: + recoveryCodes: + type: array + description: The one-time recovery codes for this TOTP factor. + items: + type: string + example: + - ABCD-EFGH-IJKL + - MNOP-QRST-UVWX + 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: + - options + - allowedOrigins + - relyingPartyId + properties: + options: type: object - description: Additional error details additionalProperties: true - Error410: + 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 + PasskeyEnrollmentConfirmRequest: + type: object + description: The WebAuthn credential a device produced for a passkey registration challenge, submitted to enroll the passkey factor. + required: + - origin + - credential + properties: + 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`. + PasskeyEnrollmentConfirmResponse: + type: object + description: The enrolled passkey factor returned after a successful confirmation. + required: + - factor + properties: + factor: + $ref: '#/components/schemas/ScaFactorView' + 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: + - factor + properties: + 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 + 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 + - '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 + ScaLoginComplete: type: object + description: The provider-reported status of a completed SCA login session. required: - - message - status - - code properties: status: - type: integer - enum: - - 410 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | CUSTOMER_DELETED | Customer has been permanently deleted | - enum: - - CUSTOMER_DELETED - message: type: string - 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. + description: The provider-reported status of the login session. + example: SUCCESS + RecordSecurityEventRequest: type: object + description: Records a client-side security-relevant event for the customer with the underlying provider's risk engine (e.g. a sign-in, a sensitive view). Used to feed the provider's adaptive-authentication signals. 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: - 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: + eventType: 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 + description: The provider-defined event type to record. + example: LOGIN + TwoFactorResetStartRequest: type: object + description: Selects which enrolled factor to reset via the liveness-gated recovery flow. 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. + - factor properties: - payloadToSign: - 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: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The enrolled factor to reset. + TwoFactorResetStart: + type: object + description: The reset handle plus the opaque provider 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). `sumsubAccessToken` and `verificationLink` are omitted when the provider does not return them. + 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. + sumsubAccessToken: + type: + - string + - 'null' + description: Access token for the embedded liveness/verification SDK, bound to this reset. Omitted when the provider does not return one. + verificationLink: + type: + - string + - 'null' + description: Hosted identity-verification page URL for completing liveness. Omitted when the provider does not return one. 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 the provider does not return one. + example: '2025-10-03T12:30:00Z' + TwoFactorResetStatus: type: object + description: The provider-reported status of an in-progress 2FA reset, polled until it reaches the provider's liveness-passed value. required: - - message - status - - code properties: status: - type: integer - enum: - - 424 - 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 | - 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: + description: The provider-reported status of the reset. + example: PENDING + ScaChallenge: type: object - description: A hosted KYC link that the customer can complete to verify their identity. + 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 or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. + + **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: + 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: The provider handle plus the SCA challenge a caller authorizes to finish trusting (or untrusting) a beneficiary. `whitelistedId` is the provider's beneficiary handle; thread it back on the confirm call so confirm need not re-whitelist (which could trigger a second challenge). `scaChallenge` is omitted when the provider does not surface one on the whitelist response; the caller then confirms without a `challengeId`. + required: + - whitelistedId + properties: + whitelistedId: 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: The provider's beneficiary (whitelist) handle. Thread it back on the confirm call. + scaChallenge: + $ref: '#/components/schemas/ScaChallenge' + description: The SCA challenge to satisfy on the confirm call. Omitted when the provider issues no challenge on the whitelist response. + BeneficiaryTrustConfirmRequest: type: object - description: Request body for confirming an email or phone verification challenge. + 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`), plus the `whitelistedId` returned by the trust start and, when the start issued one, the `challengeId`. required: - - code + - whitelistedId properties: - code: + whitelistedId: type: string - description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`. + description: The provider beneficiary handle returned as `whitelistedId` by the trust start, threaded back so confirm need not re-whitelist. + 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 + 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 diff --git a/openapi.yaml b/openapi.yaml index 1e57d7e52..9abcb3e60 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1127,180 +1127,93 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/internal-accounts: + /customers/{customerId}/sca/factors: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer whose enrolled factors are listed. + 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 whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: The customer's payment provider does not require SCA. 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 + /customers/{customerId}/sca/factors/totp: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer enrolling a TOTP factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Start TOTP 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 a time-based one-time-password (TOTP) authenticator factor + for the customer. Returns the shared secret and an `otpauth://` provisioning + URI; the customer scans it into an authenticator app and confirms with the + first code via `POST /customers/{customerId}/sca/factors/totp/confirm`. - External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform. - operationId: listCustomerExternalAccounts + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startTotpFactorEnrollment 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 responses: '200': - description: Successful operation + description: TOTP enrollment started; the shared secret is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' + $ref: '#/components/schemas/TotpEnrollmentStart' '400': - description: Bad request - Invalid parameters + description: Invalid request content: application/json: schema: @@ -1311,18 +1224,48 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/{customerId}/sca/factors/totp/confirm: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer confirming a TOTP factor. + 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 TOTP factor enrollment + description: | + Finalize TOTP factor enrollment by submitting the shared secret from the + start call and the first code the customer's authenticator app produces. + Returns one-time recovery codes shown to the customer only once. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the code is always `123456`. + operationId: confirmTotpFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1330,48 +1273,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/TotpEnrollmentConfirmRequest' responses: - '201': - description: External account created successfully + '200': + description: TOTP factor enrolled; recovery codes are returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/TotpEnrollmentConfirmResponse' '400': - description: Bad request + description: Invalid or incorrect confirmation code content: application/json: schema: @@ -1382,8 +1293,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: The customer's payment provider does not require SCA. content: application/json: schema: @@ -1394,29 +1311,44 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/external-accounts/{externalAccountId}: + /customers/{customerId}/sca/factors/passkey: parameters: - - name: externalAccountId + - name: customerId in: path - description: System-generated unique external account identifier + description: The unique identifier of the customer enrolling a passkey factor. 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 + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Start passkey factor enrollment + description: | + Begin enrolling a WebAuthn passkey factor for the customer. Returns opaque + WebAuthn registration `options`; pass them to the device's WebAuthn API and + submit the resulting credential via + `POST /customers/{customerId}/sca/factors/passkey/confirm`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startPasskeyFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] responses: '200': - description: Successful operation + description: Passkey enrollment started; WebAuthn registration options are returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/PasskeyEnrollmentStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1424,28 +1356,66 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error 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 + /customers/{customerId}/sca/factors/passkey/confirm: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer confirming a passkey factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + post: + summary: Confirm passkey factor enrollment + description: | + Finalize passkey factor enrollment by submitting the WebAuthn credential the + device produced for the registration challenge, along with the origin it was + produced against. Returns the enrolled factor. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: confirmPasskeyFactorEnrollment tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PasskeyEnrollmentConfirmRequest' responses: - '204': - description: External account deleted successfully + '200': + description: Passkey factor enrolled; the enrolled factor is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse' + '400': + description: Invalid credential or origin + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1453,82 +1423,104 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $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 + /customers/{customerId}/sca/factors/passkey/{credentialId}: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer whose passkey 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 passkey to delete (from the enrolled factor's `credentialId`). + required: true + schema: + type: string + delete: + summary: Delete an enrolled passkey factor description: | - Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer. + Delete an enrolled WebAuthn passkey factor by its credential id. - 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 whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: deletePasskeyFactor 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 responses: - '200': - description: Successful operation + '204': + description: Passkey deleted; no content is returned. + '401': + description: Unauthorized content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountListResponse' - '400': - description: Bad request - Invalid parameters + $ref: '#/components/schemas/Error401' + '404': + description: Customer or passkey not found content: application/json: schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. 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/{customerId}/sca/login/start: + parameters: + - name: customerId + in: path + 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: Add a new platform external account - description: Register a new external bank account for the platform. - operationId: createPlatformExternalAccount + 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 /customers/{customerId}/sca/login/complete`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startScaLogin tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1536,46 +1528,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/ScaLoginStartRequest' responses: - '201': - description: External account created successfully + '200': + description: SCA login started; factor-specific material is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/ScaLoginStart' '400': - description: Bad request + description: Invalid or unknown factor content: application/json: schema: @@ -1586,8 +1548,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: The customer's payment provider does not require SCA. content: application/json: schema: @@ -1598,29 +1566,52 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts/{externalAccountId}: + /customers/{customerId}/sca/login/complete: parameters: - - name: externalAccountId + - name: customerId in: path - description: System-generated unique external account identifier + description: The unique identifier of the customer completing an SCA login. 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: 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 + provider-reported session status. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: completeScaLogin tags: - - External Accounts + - Strong Customer Authentication security: - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScaLoginCompleteRequest' responses: '200': - description: Successful operation + description: SCA login completed; the session status is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/ScaLoginComplete' + '400': + description: Invalid or expired proof + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1628,28 +1619,62 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '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 + /customers/{customerId}/sca/record-event: + parameters: + - name: customerId + in: path + 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 the + underlying provider's risk engine (e.g. a sign-in, a sensitive view), to + feed adaptive-authentication signals. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: '204': - description: External account deleted successfully + description: Event recorded; no content is returned. + '400': + description: Invalid event type + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1657,25 +1682,48 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Customer not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /beneficial-owners: + /customers/{customerId}/sca/factors/reset: + parameters: + - name: customerId + in: path + 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 provider's liveness check and returns a `resetId` plus the + opaque liveness handles (`sumsubAccessToken` / `verificationLink`) the end + user completes it with. Poll + `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness + passes, then call the complete endpoint. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startTwoFactorReset tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] requestBody: @@ -1683,16 +1731,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,52 +1757,112 @@ paths: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/{customerId}/sca/factors/reset/{resetId}: + parameters: + - name: customerId + in: path + 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 provider's + liveness-passed value, after which the reset can be completed. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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 + description: The current reset status. content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwnerListResponse' + $ref: '#/components/schemas/TwoFactorResetStatus' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Customer or reset not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /customers/{customerId}/sca/factors/reset/{resetId}/complete: + parameters: + - name: customerId + in: path + 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. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: completeTwoFactorReset + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '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: @@ -1765,35 +1873,69 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' + '404': + description: Customer or reset not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '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 + /customers/{customerId}/external-accounts/{externalAccountId}/trust: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer who owns the external account. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - 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. Creates the whitelist entry and + returns a `whitelistedId` handle plus, when the provider issues one, the + `scaChallenge` to satisfy. Complete with + `POST /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startBeneficiaryTrust tags: - - KYC/KYB Verifications + - Strong Customer Authentication security: - BasicAuth: [] - parameters: - - name: beneficialOwnerId - in: path - description: Beneficial owner ID - required: true - schema: - type: string responses: '200': - description: Successful operation + description: Beneficiary trust started; the whitelist handle and any challenge are returned. content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwner' + $ref: '#/components/schemas/BeneficiaryTrustStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' '401': description: Unauthorized content: @@ -1801,47 +1943,71 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Beneficial owner not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $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 + /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer who owns the external account. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - 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 by submitting the `whitelistedId` from the + trust start and 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 whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryTrust 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 content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwnerUpdateRequest' + $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest' responses: '200': - description: Beneficial owner updated successfully + description: Beneficiary trusted. content: application/json: schema: - $ref: '#/components/schemas/BeneficialOwner' + $ref: '#/components/schemas/BeneficiaryTrustConfirm' '400': - description: Bad request - Invalid parameters + description: Invalid or expired proof content: application/json: schema: @@ -1853,40 +2019,71 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Beneficial owner not found + description: Customer or external account not found content: application/json: schema: $ref: '#/components/schemas/Error404' + '409': + description: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' - /documents: + /customers/{customerId}/external-accounts/{externalAccountId}/untrust/confirm: + parameters: + - name: customerId + in: path + description: The unique identifier of the customer who owns the external account. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - name: externalAccountId + in: path + description: The unique identifier of the external account (beneficiary) being untrusted. + required: true + schema: + type: string post: - summary: Upload a document + summary: Confirm untrusting 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. + Finalize untrusting a beneficiary by submitting the `whitelistedId` from the + trust start and the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when + one was issued. Returns `trusted: false`. - Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB. - operationId: uploadDocument + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the SMS/TOTP code is always `123456`. + operationId: confirmBeneficiaryUntrust tags: - - Documents + - Strong Customer Authentication security: - BasicAuth: [] requestBody: - $ref: '#/components/requestBodies/DocumentUploadRequestBody' + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest' responses: - '201': - description: Document uploaded successfully + '200': + 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: @@ -1898,33 +2095,56 @@ 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: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' '500': description: Internal service error content: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/internal-accounts: get: - summary: List documents + summary: List Customer internal accounts description: | - Retrieve a list of documents with optional filtering by document holder. - operationId: listDocuments + 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: documentHolder + - name: currency in: query - description: Filter by document holder ID (Customer or BeneficialOwner) + 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) @@ -1946,7 +2166,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DocumentListResponse' + $ref: '#/components/schemas/InternalAccountListResponse' '400': description: Bad request - Invalid parameters content: @@ -1965,237 +2185,38 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /documents/{documentId}: + /platform/internal-accounts: get: - summary: Get a document by ID - description: Retrieve details and metadata of a specific document by ID. - operationId: getDocument + 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: - - 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 - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Document' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Document not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - put: - summary: Replace a document - 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 - tags: - - Documents - security: - - BasicAuth: [] - parameters: - - name: documentId - in: path - description: Document ID - required: true - schema: - type: string - requestBody: - $ref: '#/components/requestBodies/DocumentReplaceRequestBody' - responses: - '200': - description: Document replaced successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Document' - '400': - description: Bad request - Invalid file type, size, or parameters - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Document 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 a document - description: | - Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable. - operationId: deleteDocument - tags: - - Documents - security: - - BasicAuth: [] - parameters: - - name: documentId - in: path - description: Document ID - required: true + - 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: - type: string - responses: - '204': - description: Document deleted successfully - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Document not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: Conflict - Document cannot be deleted (already submitted for verification) - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /verifications: - post: - summary: Submit customer for verification - 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 - - **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: - - KYC/KYB Verifications - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/VerificationRequest' + $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 +2229,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 +2283,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/VerificationListResponse' + $ref: '#/components/schemas/ExternalAccountListResponse' '400': description: Bad request - Invalid parameters content: @@ -2282,94 +2302,106 @@ 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 + requestBody: + required: true + 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 responses: - '200': - description: Successful operation + '201': + description: External account created successfully content: application/json: schema: - $ref: '#/components/schemas/Verification' + $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' - '404': - description: Verification 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-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 + /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/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: - '201': - description: Transfer-in request created successfully - 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: @@ -2377,7 +2409,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Customer or account not found + description: External account not found content: application/json: schema: @@ -2388,60 +2420,17 @@ paths: 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 + delete: + summary: Delete customer external account by ID + description: Delete a customer external account by its system-generated ID + operationId: deleteCustomerExternalAccountById 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 - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' + '204': + description: External account deleted successfully '401': description: Unauthorized content: @@ -2449,7 +2438,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Customer or account not found + description: External account not found content: application/json: schema: @@ -2460,45 +2449,49 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /receiver/uma/{receiverUmaAddress}: + /platform/external-accounts: get: - summary: Look up an UMA address for payment + summary: List platform external accounts 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 + 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: receiverUmaAddress - in: path - description: UMA address of the intended recipient - required: true + - name: currency + in: query + description: Filter by currency code + required: false schema: type: string - - 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/ReceiverUmaLookupResponse' + $ref: '#/components/schemas/ExternalAccountListResponse' '400': - description: Bad request - Missing or invalid parameters + description: Bad request - Invalid parameters content: application/json: schema: @@ -2509,70 +2502,65 @@ paths: application/json: 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`). - 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}: - get: - summary: Look up an external account for payment - 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 + post: + summary: Add a new platform external account + description: Register a new external bank account for the platform. + operationId: createPlatformExternalAccount tags: - - Cross-Currency Transfers + - External Accounts security: - BasicAuth: [] - parameters: - - name: accountId - in: path - description: System-generated ID of the external account - required: true - schema: - type: string - 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 + 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: - '200': - description: Successful lookup + '201': + description: External account created successfully content: application/json: schema: - $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse' + $ref: '#/components/schemas/ExternalAccount' '400': - description: Bad request - Missing or invalid parameters + description: Bad request content: application/json: schema: @@ -2583,56 +2571,70 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '404': - description: External account not found + '409': + description: Conflict - External account already exists content: application/json: schema: - $ref: '#/components/schemas/Error404' - '412': - description: Counterparty doesn't support UMA version + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error content: application/json: schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue + $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: [] + responses: + '200': + description: Successful operation content: application/json: schema: - $ref: '#/components/schemas/Error424' + $ref: '#/components/schemas/ExternalAccount' + '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' - /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 + delete: + summary: Delete platform external account by ID + description: Delete a platform external account by its system-generated ID + operationId: deletePlatformExternalAccountById tags: - - Cross-Currency Transfers + - 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 - content: - application/json: - schema: - $ref: '#/components/schemas/Quote' + '204': + description: External account deleted successfully '401': description: Unauthorized content: @@ -2640,7 +2642,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Quote not found + description: External account not found content: application/json: schema: @@ -2651,132 +2653,31 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /quotes: + /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 +2688,145 @@ paths: application/json: schema: $ref: '#/components/schemas/Error401' - '412': - description: Counterparty doesn't support UMA version - content: - application/json: - schema: - $ref: '#/components/schemas/Error412' - '424': - description: Counterparty issue + '404': + description: Customer not found 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 +2838,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 + Retrieve a list of documents with optional filtering by document holder. + operationId: listDocuments tags: - - Transactions + - 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 +2925,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 +2950,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 +2980,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: @@ -3218,38 +2991,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 +3029,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Transaction not found + description: Document not found content: application/json: schema: @@ -3272,43 +3040,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 +3066,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 +3083,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 + description: | + Verification status returned. Check `verificationStatus` and `errors` to determine next steps. content: application/json: schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: + $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/Error401' - '404': - description: Transaction not found + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized 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/Error401' + '404': + description: Customer 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' - /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 +3261,168 @@ 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. + + 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/UmaInvitation' + $ref: '#/components/schemas/TransactionOneOf' '400': - description: Bad request + description: Bad request - Invalid parameters content: application/json: schema: @@ -3658,36 +3433,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 +3495,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 +3569,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 +3636,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 + 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: - - Sandbox + - 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 +3772,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 +3948,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 +4127,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 +4162,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 +4184,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ApiToken' + $ref: '#/components/schemas/TransactionOneOf' '401': description: Unauthorized content: @@ -4238,7 +4192,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Token not found + description: Transaction not found content: application/json: schema: @@ -4249,19 +4203,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 +4246,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Token not found + description: Transaction not found content: application/json: schema: @@ -4284,368 +4257,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 +4435,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 +4469,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. + description: CSV upload accepted for processing 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. - 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 +4643,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': - description: Internal service error - content: + '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 +4735,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 +4752,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 +4814,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 +4845,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 +4904,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 +4993,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 +5082,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 +5109,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 +5174,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AgentListResponse' + $ref: '#/components/schemas/TokenListResponse' '400': description: Bad request - Invalid parameters content: @@ -5641,153 +5193,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 +5389,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 +5459,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 +5476,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 +5647,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 +5713,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 + $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/Error404' - '500': - description: Internal service error + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized 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/Error401' + '404': + description: Authentication credential not found content: application/json: schema: - $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse' - '400': - description: Bad request (e.g., code already redeemed or expired) - content: - application/json: + $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: - $ref: '#/components/schemas/Error400' - '404': - description: Device code not found + 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 +6075,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 +6145,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 +6250,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 +6354,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 +6433,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Action not found + description: Delegated key not found content: application/json: schema: @@ -6629,69 +6444,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 +6490,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 +6525,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 +6607,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 +6626,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 +6678,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 +6713,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 +6772,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 +6829,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 +6846,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Agent not found content: application/json: schema: @@ -7035,18 +6857,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 +6879,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 +6899,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 +6947,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 +7010,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 +7094,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 +7136,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 +7178,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 +7283,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 +7331,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 +7377,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 +7442,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 +7521,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 +7559,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 +7572,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 +7603,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Stablecoin provider account link not found + description: Action not found content: application/json: schema: @@ -7801,290 +7614,1462 @@ 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. + 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: + - Agent Operations + security: + - 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: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/InternalAccountListResponse' + '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/external-accounts: + get: + summary: List agent external accounts + 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 + tags: + - Agent Operations + security: + - 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: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccountListResponse' + '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' + post: + summary: Add an external account + 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 + 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 + 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' + /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: | + 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: + - Agent Operations + security: + - AgentAuth: [] + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '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' + 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: Issue a card + 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 + tags: + - Cards + security: + - BasicAuth: [] + requestBody: + required: true + 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 + 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 @@ -10688,22 +11673,202 @@ components: BusinessCustomerCreateRequest: title: Business Customer Create Request allOf: - - $ref: '#/components/schemas/CustomerCreateRequest' + - $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 | + | 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 + 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: + - message + - status + - code + properties: + status: + type: integer + enum: + - 410 + description: HTTP status code + code: + type: string + description: | + | Error Code | Description | + |------------|-------------| + | CUSTOMER_DELETED | Customer has been permanently deleted | + enum: + - CUSTOMER_DELETED + message: + type: string + 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 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 + 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' - - type: object - properties: - businessInfo: - $ref: '#/components/schemas/BusinessInfo' - CustomerCreateRequestOneOf: + CustomerUpdateRequestOneOf: oneOf: - - $ref: '#/components/schemas/IndividualCustomerCreateRequest' - - $ref: '#/components/schemas/BusinessCustomerCreateRequest' + - $ref: '#/components/schemas/IndividualCustomerUpdateRequest' + - $ref: '#/components/schemas/BusinessCustomerUpdateRequest' discriminator: propertyName: customerType mapping: - INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest' - BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest' - Error409: + INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest' + BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest' + SignedRequestChallenge: + title: Signed Request Challenge + type: object + 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. + properties: + payloadToSign: + 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: + 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 + expiresAt: + type: string + 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: type: object required: - message @@ -10713,24 +11878,24 @@ components: status: type: integer enum: - - 409 + - 424 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 | - | 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 | + | 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: - - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL - - UMA_ADDRESS_EXISTS - - EMAIL_OTP_EMAIL_ALREADY_EXISTS - - EMAIL_OTP_CREDENTIAL_SET_CHANGED - - CONFLICT + - 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 @@ -10738,234 +11903,445 @@ components: type: object description: Additional error details additionalProperties: true - Error404: + 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: + - kycUrl + - expiresAt + - provider + properties: + kycUrl: + 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 + 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: - - 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: + 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: + - factor + properties: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The kind of enrolled factor. + credentialId: + type: + - string + - 'null' + 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 + - 'null' + 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: + $ref: '#/components/schemas/ScaFactorView' + TotpEnrollmentStart: + type: object + description: The shared secret a customer's authenticator app needs to enroll a TOTP factor. Returned by `POST /customers/{customerId}/sca/factors/totp`; the customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with the first code their app produces. + required: + - secret + - secretBase32Encoded + - totpUri + properties: + secret: type: string - description: Error message - details: + 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 + 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: + - secret + - code + properties: + 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' + 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: + - recoveryCodes + properties: + recoveryCodes: + type: array + description: The one-time recovery codes for this TOTP factor. + items: + type: string + example: + - ABCD-EFGH-IJKL + - MNOP-QRST-UVWX + 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: + - options + - allowedOrigins + - relyingPartyId + properties: + options: type: object - description: Additional error details additionalProperties: true - Error410: + 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 + PasskeyEnrollmentConfirmRequest: + type: object + description: The WebAuthn credential a device produced for a passkey registration challenge, submitted to enroll the passkey factor. + required: + - origin + - credential + properties: + 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`. + PasskeyEnrollmentConfirmResponse: + type: object + description: The enrolled passkey factor returned after a successful confirmation. + required: + - factor + properties: + factor: + $ref: '#/components/schemas/ScaFactorView' + 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: + - factor + properties: + 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 + 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 + - '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 + ScaLoginComplete: type: object + description: The provider-reported status of a completed SCA login session. required: - - message - status - - code properties: status: - type: integer - enum: - - 410 - description: HTTP status code - code: - type: string - description: | - | Error Code | Description | - |------------|-------------| - | CUSTOMER_DELETED | Customer has been permanently deleted | - enum: - - CUSTOMER_DELETED - message: type: string - 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. + description: The provider-reported status of the login session. + example: SUCCESS + RecordSecurityEventRequest: type: object + description: Records a client-side security-relevant event for the customer with the underlying provider's risk engine (e.g. a sign-in, a sensitive view). Used to feed the provider's adaptive-authentication signals. 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: - 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: + eventType: 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 + description: The provider-defined event type to record. + example: LOGIN + TwoFactorResetStartRequest: type: object + description: Selects which enrolled factor to reset via the liveness-gated recovery flow. 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. + - factor properties: - payloadToSign: - 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: + factor: + $ref: '#/components/schemas/ScaFactor' + description: The enrolled factor to reset. + TwoFactorResetStart: + type: object + description: The reset handle plus the opaque provider 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). `sumsubAccessToken` and `verificationLink` are omitted when the provider does not return them. + 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. + sumsubAccessToken: + type: + - string + - 'null' + description: Access token for the embedded liveness/verification SDK, bound to this reset. Omitted when the provider does not return one. + verificationLink: + type: + - string + - 'null' + description: Hosted identity-verification page URL for completing liveness. Omitted when the provider does not return one. 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 the provider does not return one. + example: '2025-10-03T12:30:00Z' + TwoFactorResetStatus: type: object + description: The provider-reported status of an in-progress 2FA reset, polled until it reaches the provider's liveness-passed value. required: - - message - status - - code properties: status: - type: integer - enum: - - 424 - 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 | - 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: + description: The provider-reported status of the reset. + example: PENDING + ScaChallenge: type: object - description: A hosted KYC link that the customer can complete to verify their identity. + 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 or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. + + **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: + 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: The provider handle plus the SCA challenge a caller authorizes to finish trusting (or untrusting) a beneficiary. `whitelistedId` is the provider's beneficiary handle; thread it back on the confirm call so confirm need not re-whitelist (which could trigger a second challenge). `scaChallenge` is omitted when the provider does not surface one on the whitelist response; the caller then confirms without a `challengeId`. + required: + - whitelistedId + properties: + whitelistedId: 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: The provider's beneficiary (whitelist) handle. Thread it back on the confirm call. + scaChallenge: + $ref: '#/components/schemas/ScaChallenge' + description: The SCA challenge to satisfy on the confirm call. Omitted when the provider issues no challenge on the whitelist response. + BeneficiaryTrustConfirmRequest: type: object - description: Request body for confirming an email or phone verification challenge. + 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`), plus the `whitelistedId` returned by the trust start and, when the start issued one, the `challengeId`. required: - - code + - whitelistedId properties: - code: + whitelistedId: type: string - description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`. + description: The provider beneficiary handle returned as `whitelistedId` by the trust start, threaded back so confirm need not re-whitelist. + 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 + 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 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..bf934f1ab --- /dev/null +++ b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml @@ -0,0 +1,47 @@ +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`), plus the + `whitelistedId` returned by the trust start and, when the start issued one, + the `challengeId`. +required: + - whitelistedId +properties: + whitelistedId: + type: string + description: >- + The provider beneficiary handle returned as `whitelistedId` by the trust + start, threaded back so confirm need not re-whitelist. + 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..389574c70 --- /dev/null +++ b/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml @@ -0,0 +1,21 @@ +type: object +description: >- + The provider handle plus the SCA challenge a caller authorizes to finish + trusting (or untrusting) a beneficiary. `whitelistedId` is the provider's + beneficiary handle; thread it back on the confirm call so confirm need not + re-whitelist (which could trigger a second challenge). `scaChallenge` is + omitted when the provider does not surface one on the whitelist response; the + caller then confirms without a `challengeId`. +required: + - whitelistedId +properties: + whitelistedId: + type: string + description: >- + The provider's beneficiary (whitelist) handle. Thread it back on the + confirm call. + scaChallenge: + $ref: ./ScaChallenge.yaml + description: >- + The SCA challenge to satisfy on the confirm call. Omitted when the + provider issues no challenge on the whitelist response. diff --git a/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml new file mode 100644 index 000000000..87e018798 --- /dev/null +++ b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml @@ -0,0 +1,20 @@ +type: object +description: >- + The WebAuthn credential a device produced for a passkey registration + challenge, submitted to enroll the passkey factor. +required: + - origin + - credential +properties: + 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..78f8431fa --- /dev/null +++ b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml @@ -0,0 +1,7 @@ +type: object +description: The enrolled passkey factor returned after a successful confirmation. +required: + - factor +properties: + 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..ed2a68d02 --- /dev/null +++ b/openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml @@ -0,0 +1,31 @@ +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: + - options + - allowedOrigins + - relyingPartyId +properties: + 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/RecordSecurityEventRequest.yaml b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml new file mode 100644 index 000000000..b3804691a --- /dev/null +++ b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml @@ -0,0 +1,12 @@ +type: object +description: >- + Records a client-side security-relevant event for the customer with the + underlying provider's risk engine (e.g. a sign-in, a sensitive view). Used to + feed the provider's adaptive-authentication signals. +required: + - eventType +properties: + eventType: + type: string + description: The provider-defined event type to record. + example: LOGIN 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..b9d7e9862 --- /dev/null +++ b/openapi/components/schemas/sca/ScaFactorView.yaml @@ -0,0 +1,21 @@ +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 + - 'null' + 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 + - 'null' + 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..47eed1da9 --- /dev/null +++ b/openapi/components/schemas/sca/ScaLoginComplete.yaml @@ -0,0 +1,9 @@ +type: object +description: The provider-reported status of a completed SCA login session. +required: + - status +properties: + status: + type: string + description: The provider-reported status of the login session. + example: SUCCESS diff --git a/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml b/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml new file mode 100644 index 000000000..2d2ddefea --- /dev/null +++ b/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml @@ -0,0 +1,45 @@ +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 +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..9f3e87a21 --- /dev/null +++ b/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml @@ -0,0 +1,20 @@ +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: + - secret + - code +properties: + 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..9aba66bb2 --- /dev/null +++ b/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml @@ -0,0 +1,16 @@ +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: + - recoveryCodes +properties: + 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..cd5e6efc3 --- /dev/null +++ b/openapi/components/schemas/sca/TotpEnrollmentStart.yaml @@ -0,0 +1,25 @@ +type: object +description: >- + The shared secret a customer's authenticator app needs to enroll a TOTP + factor. Returned by `POST /customers/{customerId}/sca/factors/totp`; the + customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with + the first code their app produces. +required: + - secret + - secretBase32Encoded + - totpUri +properties: + 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/TwoFactorResetStart.yaml b/openapi/components/schemas/sca/TwoFactorResetStart.yaml new file mode 100644 index 000000000..8e7f099aa --- /dev/null +++ b/openapi/components/schemas/sca/TwoFactorResetStart.yaml @@ -0,0 +1,36 @@ +type: object +description: >- + The reset handle plus the opaque provider 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). `sumsubAccessToken` and + `verificationLink` are omitted when the provider does not return them. +required: + - resetId +properties: + resetId: + type: string + description: >- + Identifier for this reset; pass it to the status and complete endpoints. + sumsubAccessToken: + type: + - string + - 'null' + description: >- + Access token for the embedded liveness/verification SDK, bound to this + reset. Omitted when the provider does not return one. + verificationLink: + type: + - string + - 'null' + description: >- + Hosted identity-verification page URL for completing liveness. Omitted + when the provider does not return one. + expiresAt: + type: + - string + - 'null' + format: date-time + description: >- + Absolute UTC timestamp at the end of the reset window. Omitted when the + provider does not return one. + 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..bf58496bb --- /dev/null +++ b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml @@ -0,0 +1,11 @@ +type: object +description: >- + The provider-reported status of an in-progress 2FA reset, polled until it + reaches the provider's liveness-passed value. +required: + - status +properties: + status: + type: string + description: The provider-reported status of the reset. + example: PENDING diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index a4865267f..3bcab4dff 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -153,6 +153,36 @@ paths: $ref: paths/customers/customers_{customerId}_verify-phone.yaml /customers/{customerId}/verify-phone/confirm: $ref: paths/customers/customers_{customerId}_verify-phone_confirm.yaml + /customers/{customerId}/sca/factors: + $ref: paths/customers/customers_{customerId}_sca_factors.yaml + /customers/{customerId}/sca/factors/totp: + $ref: paths/customers/customers_{customerId}_sca_factors_totp.yaml + /customers/{customerId}/sca/factors/totp/confirm: + $ref: paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml + /customers/{customerId}/sca/factors/passkey: + $ref: paths/customers/customers_{customerId}_sca_factors_passkey.yaml + /customers/{customerId}/sca/factors/passkey/confirm: + $ref: paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml + /customers/{customerId}/sca/factors/passkey/{credentialId}: + $ref: paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml + /customers/{customerId}/sca/login/start: + $ref: paths/customers/customers_{customerId}_sca_login_start.yaml + /customers/{customerId}/sca/login/complete: + $ref: paths/customers/customers_{customerId}_sca_login_complete.yaml + /customers/{customerId}/sca/record-event: + $ref: paths/customers/customers_{customerId}_sca_record-event.yaml + /customers/{customerId}/sca/factors/reset: + $ref: paths/customers/customers_{customerId}_sca_factors_reset.yaml + /customers/{customerId}/sca/factors/reset/{resetId}: + $ref: paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml + /customers/{customerId}/sca/factors/reset/{resetId}/complete: + $ref: paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml + /customers/{customerId}/external-accounts/{externalAccountId}/trust: + $ref: paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml + /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm: + $ref: paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml + /customers/{customerId}/external-accounts/{externalAccountId}/untrust/confirm: + $ref: paths/customers/customers_{customerId}_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_{customerId}_external-accounts_{externalAccountId}_trust.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml new file mode 100644 index 000000000..c95abb320 --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml @@ -0,0 +1,68 @@ +parameters: + - name: customerId + in: path + description: The unique identifier of the customer who owns the external account. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - 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. Creates the whitelist entry and + returns a `whitelistedId` handle plus, when the provider issues one, the + `scaChallenge` to satisfy. Complete with + `POST /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startBeneficiaryTrust + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '200': + description: Beneficiary trust started; the whitelist handle and any challenge are 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: The customer's payment provider does not require SCA. + 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_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml new file mode 100644 index 000000000..33419aad4 --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml @@ -0,0 +1,75 @@ +parameters: + - name: customerId + in: path + description: The unique identifier of the customer who owns the external account. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - 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 by submitting the `whitelistedId` from the + trust start and 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 whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: The customer's payment provider does not require SCA. + 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_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml new file mode 100644 index 000000000..ad10c74bd --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml @@ -0,0 +1,75 @@ +parameters: + - name: customerId + in: path + description: The unique identifier of the customer who owns the external account. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 + - 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 by submitting the `whitelistedId` from the + trust start and 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 whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_factors.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors.yaml new file mode 100644 index 000000000..5e1b1eecd --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_factors.yaml @@ -0,0 +1,52 @@ +parameters: + - name: customerId + in: path + description: The unique identifier of the customer whose enrolled factors are listed. + 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 whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_factors_passkey.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml new file mode 100644 index 000000000..7f82a1dd4 --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml @@ -0,0 +1,61 @@ +parameters: + - name: customerId + in: path + description: The unique identifier of the customer enrolling a passkey factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Start passkey factor enrollment + description: | + Begin enrolling a WebAuthn passkey factor for the customer. Returns opaque + WebAuthn registration `options`; pass them to the device's WebAuthn API and + submit the resulting credential via + `POST /customers/{customerId}/sca/factors/passkey/confirm`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startPasskeyFactorEnrollment + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '200': + description: Passkey enrollment started; WebAuthn registration options are returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/PasskeyEnrollmentStart.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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_factors_passkey_confirm.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml new file mode 100644 index 000000000..9842acaa2 --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml @@ -0,0 +1,66 @@ +parameters: + - name: customerId + in: path + description: The unique identifier of the customer confirming a passkey factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Confirm passkey factor enrollment + description: | + Finalize passkey factor enrollment by submitting the WebAuthn credential the + device produced for the registration challenge, along with the origin it was + produced against. Returns the enrolled factor. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: confirmPasskeyFactorEnrollment + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml + responses: + '200': + description: Passkey factor enrolled; the enrolled factor is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml + '400': + description: Invalid credential or origin + 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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_factors_passkey_{credentialId}.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml new file mode 100644 index 000000000..101823d9d --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml @@ -0,0 +1,54 @@ +parameters: + - name: customerId + in: path + description: The unique identifier of the customer whose passkey 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 passkey to delete (from the enrolled factor's `credentialId`). + required: true + schema: + type: string +delete: + summary: Delete an enrolled passkey factor + description: | + Delete an enrolled WebAuthn passkey factor by its credential id. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: deletePasskeyFactor + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '204': + description: Passkey deleted; no content is returned. + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Customer or passkey not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: The customer's payment provider does not require SCA. + 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_{customerId}_sca_factors_reset.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml new file mode 100644 index 000000000..ccd682e9d --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml @@ -0,0 +1,69 @@ +parameters: + - name: customerId + in: path + 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 provider's liveness check and returns a `resetId` plus the + opaque liveness handles (`sumsubAccessToken` / `verificationLink`) the end + user completes it with. Poll + `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness + passes, then call the complete endpoint. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_factors_reset_{resetId}.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml new file mode 100644 index 000000000..7261d949f --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml @@ -0,0 +1,59 @@ +parameters: + - name: customerId + in: path + 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 provider's + liveness-passed value, after which the reset can be completed. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_factors_reset_{resetId}_complete.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml new file mode 100644 index 000000000..1fa43be1b --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml @@ -0,0 +1,61 @@ +parameters: + - name: customerId + in: path + 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. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: completeTwoFactorReset + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + 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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_factors_totp.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml new file mode 100644 index 000000000..87de36893 --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml @@ -0,0 +1,61 @@ +parameters: + - name: customerId + in: path + description: The unique identifier of the customer enrolling a TOTP factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Start TOTP factor enrollment + description: | + Begin enrolling a time-based one-time-password (TOTP) authenticator factor + for the customer. Returns the shared secret and an `otpauth://` provisioning + URI; the customer scans it into an authenticator app and confirms with the + first code via `POST /customers/{customerId}/sca/factors/totp/confirm`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + operationId: startTotpFactorEnrollment + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + responses: + '200': + description: TOTP enrollment started; the shared secret is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/TotpEnrollmentStart.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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_factors_totp_confirm.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml new file mode 100644 index 000000000..6c6c87c6e --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml @@ -0,0 +1,68 @@ +parameters: + - name: customerId + in: path + description: The unique identifier of the customer confirming a TOTP factor. + required: true + schema: + type: string + example: Customer:019542f5-b3e7-1d02-0000-000000000001 +post: + summary: Confirm TOTP factor enrollment + description: | + Finalize TOTP factor enrollment by submitting the shared secret from the + start call and the first code the customer's authenticator app produces. + Returns one-time recovery codes shown to the customer only once. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, this returns `409`. + + In sandbox, the code is always `123456`. + operationId: confirmTotpFactorEnrollment + tags: + - Strong Customer Authentication + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/sca/TotpEnrollmentConfirmRequest.yaml + responses: + '200': + description: TOTP factor enrolled; recovery codes are returned. + content: + application/json: + schema: + $ref: ../../components/schemas/sca/TotpEnrollmentConfirmResponse.yaml + '400': + description: Invalid or incorrect confirmation code + 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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_login_complete.yaml b/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml new file mode 100644 index 000000000..82274a112 --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml @@ -0,0 +1,69 @@ +parameters: + - name: customerId + in: path + 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 + provider-reported session status. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_login_start.yaml b/openapi/paths/customers/customers_{customerId}_sca_login_start.yaml new file mode 100644 index 000000000..2c1be8a0e --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_login_start.yaml @@ -0,0 +1,70 @@ +parameters: + - name: customerId + in: path + 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 /customers/{customerId}/sca/login/complete`. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: The customer's payment provider does not require SCA. + 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_{customerId}_sca_record-event.yaml b/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml new file mode 100644 index 000000000..ac573c2bc --- /dev/null +++ b/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml @@ -0,0 +1,62 @@ +parameters: + - name: customerId + in: path + 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 the + underlying provider's risk engine (e.g. a sign-in, a sensitive view), to + feed adaptive-authentication signals. + + This endpoint is only meaningful for customers whose payment provider + requires SCA (e.g. EU customers). For customers whose provider has no such + requirement, 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: + '204': + description: Event recorded; no content is returned. + '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: The customer's payment provider does not require SCA. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml From 160f227a4f30e15b08cefde7955d0e32f2d8ce08 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Wed, 17 Jun 2026 16:01:53 -0700 Subject: [PATCH 02/10] docs(sca): document terminal status values for reset/login polling Name the provider-reported terminal sentinels (LIVENESS_PASSED for 2FA reset, SUCCESS for login) in the status field descriptions so consumers can write polling/branching logic without out-of-band knowledge, while keeping the field a verbatim provider passthrough rather than a locked enum. Co-Authored-By: Claude Opus 4.8 --- mintlify/openapi.yaml | 4 ++-- openapi.yaml | 4 ++-- openapi/components/schemas/sca/ScaLoginComplete.yaml | 6 +++++- openapi/components/schemas/sca/TwoFactorResetStatus.yaml | 8 +++++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 9abcb3e60..3c4866f26 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -12178,7 +12178,7 @@ components: properties: status: type: string - description: The provider-reported status of the login session. + description: The provider-reported status of the login session, passed through verbatim (Grid does not normalize it). A successful login reports `SUCCESS`; other provider-specific values indicate the login did not complete and should be surfaced to the caller. example: SUCCESS RecordSecurityEventRequest: type: object @@ -12233,7 +12233,7 @@ components: properties: status: type: string - description: The provider-reported status of the reset. + description: The provider-reported status of the reset, passed through verbatim (Grid does not normalize it). Keep polling while it reports a non-terminal value such as `PENDING`; once it reaches the liveness-passed value (`LIVENESS_PASSED`), call the complete endpoint. Other provider-specific values may appear, so treat anything other than the liveness-passed sentinel as "not ready yet" rather than failing. example: PENDING ScaChallenge: type: object diff --git a/openapi.yaml b/openapi.yaml index 9abcb3e60..3c4866f26 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -12178,7 +12178,7 @@ components: properties: status: type: string - description: The provider-reported status of the login session. + description: The provider-reported status of the login session, passed through verbatim (Grid does not normalize it). A successful login reports `SUCCESS`; other provider-specific values indicate the login did not complete and should be surfaced to the caller. example: SUCCESS RecordSecurityEventRequest: type: object @@ -12233,7 +12233,7 @@ components: properties: status: type: string - description: The provider-reported status of the reset. + description: The provider-reported status of the reset, passed through verbatim (Grid does not normalize it). Keep polling while it reports a non-terminal value such as `PENDING`; once it reaches the liveness-passed value (`LIVENESS_PASSED`), call the complete endpoint. Other provider-specific values may appear, so treat anything other than the liveness-passed sentinel as "not ready yet" rather than failing. example: PENDING ScaChallenge: type: object diff --git a/openapi/components/schemas/sca/ScaLoginComplete.yaml b/openapi/components/schemas/sca/ScaLoginComplete.yaml index 47eed1da9..f82f642d5 100644 --- a/openapi/components/schemas/sca/ScaLoginComplete.yaml +++ b/openapi/components/schemas/sca/ScaLoginComplete.yaml @@ -5,5 +5,9 @@ required: properties: status: type: string - description: The provider-reported status of the login session. + description: >- + The provider-reported status of the login session, passed through + verbatim (Grid does not normalize it). A successful login reports + `SUCCESS`; other provider-specific values indicate the login did not + complete and should be surfaced to the caller. example: SUCCESS diff --git a/openapi/components/schemas/sca/TwoFactorResetStatus.yaml b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml index bf58496bb..ac92f73da 100644 --- a/openapi/components/schemas/sca/TwoFactorResetStatus.yaml +++ b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml @@ -7,5 +7,11 @@ required: properties: status: type: string - description: The provider-reported status of the reset. + description: >- + The provider-reported status of the reset, passed through verbatim (Grid + does not normalize it). Keep polling while it reports a non-terminal value + such as `PENDING`; once it reaches the liveness-passed value + (`LIVENESS_PASSED`), call the complete endpoint. Other provider-specific + values may appear, so treat anything other than the liveness-passed + sentinel as "not ready yet" rather than failing. example: PENDING From 23f214f08a9bfe5f020bc307e093322d97f2cb45 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Thu, 9 Jul 2026 00:34:36 -0700 Subject: [PATCH 03/10] docs(sca): enforce proof, provider-neutral liveness token, tighten mgmt schemas Address faraday review on #600: - Add an anyOf (code XOR passkeyAssertion+origin) to BeneficiaryTrustConfirmRequest and ScaLoginCompleteRequest so a proof-less body fails schema validation, not just at runtime (matching lint-ignore precedent for required-on-shared-props). - Rename sumsubAccessToken -> livenessAccessToken (TwoFactorResetStart + reset start path) to keep the surface provider-neutral; the sparkcore response key is renamed in lockstep. - Document whitelistedId provenance (scoped to this account's own trust start, not reusable across accounts or after expiry). - Clarify eventType is a provider-defined closed vocabulary, not free-form input. Co-Authored-By: Claude --- .redocly.lint-ignore.yaml | 8 +++++++ mintlify/openapi.yaml | 24 ++++++++++++++----- openapi.yaml | 24 ++++++++++++++----- .../sca/BeneficiaryTrustConfirmRequest.yaml | 13 ++++++++-- .../sca/RecordSecurityEventRequest.yaml | 8 ++++++- .../schemas/sca/ScaLoginCompleteRequest.yaml | 6 +++++ .../schemas/sca/TwoFactorResetStart.yaml | 10 ++++---- ...tomers_{customerId}_sca_factors_reset.yaml | 2 +- 8 files changed, 74 insertions(+), 21 deletions(-) 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 3c4866f26..cc47de2f7 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -1713,7 +1713,7 @@ paths: description: | Begin recovering a lost enrolled factor via a liveness-gated, poll-based flow. Opens the provider's liveness check and returns a `resetId` plus the - opaque liveness handles (`sumsubAccessToken` / `verificationLink`) the end + opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end user completes it with. Poll `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness passes, then call the complete endpoint. @@ -12143,6 +12143,12 @@ components: 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' @@ -12188,7 +12194,7 @@ components: properties: eventType: type: string - description: The provider-defined event type to record. + description: A provider-defined security-event identifier drawn from the provider's accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is forwarded verbatim to the provider's risk engine, which rejects values outside that set. Left as a string (rather than an enum) so new provider-defined events don't require a spec change; treat it as a closed vocabulary defined by the provider, not free-form input. example: LOGIN TwoFactorResetStartRequest: type: object @@ -12201,18 +12207,18 @@ components: description: The enrolled factor to reset. TwoFactorResetStart: type: object - description: The reset handle plus the opaque provider 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). `sumsubAccessToken` and `verificationLink` are omitted when the provider does not return them. + description: The reset handle plus the opaque provider 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 the provider does not return them. required: - resetId properties: resetId: type: string description: Identifier for this reset; pass it to the status and complete endpoints. - sumsubAccessToken: + livenessAccessToken: type: - string - 'null' - description: Access token for the embedded liveness/verification SDK, bound to this reset. Omitted when the provider does not return one. + description: Provider-neutral access token for the embedded liveness/verification SDK, bound to this reset. Omitted when the provider does not return one. verificationLink: type: - string @@ -12306,10 +12312,16 @@ components: 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`), plus the `whitelistedId` returned by the trust start and, when the start issued one, the `challengeId`. required: - whitelistedId + anyOf: + - required: + - code + - required: + - passkeyAssertion + - origin properties: whitelistedId: type: string - description: The provider beneficiary handle returned as `whitelistedId` by the trust start, threaded back so confirm need not re-whitelist. + description: The provider beneficiary handle returned as `whitelistedId` by *this account's own* trust start, threaded back so confirm need not re-whitelist. It is scoped to that ceremony — the server rejects a `whitelistedId` issued for a different external account or from an expired/stale start, so do not cache or reuse it across accounts. challengeId: type: - string diff --git a/openapi.yaml b/openapi.yaml index 3c4866f26..cc47de2f7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1713,7 +1713,7 @@ paths: description: | Begin recovering a lost enrolled factor via a liveness-gated, poll-based flow. Opens the provider's liveness check and returns a `resetId` plus the - opaque liveness handles (`sumsubAccessToken` / `verificationLink`) the end + opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end user completes it with. Poll `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness passes, then call the complete endpoint. @@ -12143,6 +12143,12 @@ components: 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' @@ -12188,7 +12194,7 @@ components: properties: eventType: type: string - description: The provider-defined event type to record. + description: A provider-defined security-event identifier drawn from the provider's accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is forwarded verbatim to the provider's risk engine, which rejects values outside that set. Left as a string (rather than an enum) so new provider-defined events don't require a spec change; treat it as a closed vocabulary defined by the provider, not free-form input. example: LOGIN TwoFactorResetStartRequest: type: object @@ -12201,18 +12207,18 @@ components: description: The enrolled factor to reset. TwoFactorResetStart: type: object - description: The reset handle plus the opaque provider 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). `sumsubAccessToken` and `verificationLink` are omitted when the provider does not return them. + description: The reset handle plus the opaque provider 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 the provider does not return them. required: - resetId properties: resetId: type: string description: Identifier for this reset; pass it to the status and complete endpoints. - sumsubAccessToken: + livenessAccessToken: type: - string - 'null' - description: Access token for the embedded liveness/verification SDK, bound to this reset. Omitted when the provider does not return one. + description: Provider-neutral access token for the embedded liveness/verification SDK, bound to this reset. Omitted when the provider does not return one. verificationLink: type: - string @@ -12306,10 +12312,16 @@ components: 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`), plus the `whitelistedId` returned by the trust start and, when the start issued one, the `challengeId`. required: - whitelistedId + anyOf: + - required: + - code + - required: + - passkeyAssertion + - origin properties: whitelistedId: type: string - description: The provider beneficiary handle returned as `whitelistedId` by the trust start, threaded back so confirm need not re-whitelist. + description: The provider beneficiary handle returned as `whitelistedId` by *this account's own* trust start, threaded back so confirm need not re-whitelist. It is scoped to that ceremony — the server rejects a `whitelistedId` issued for a different external account or from an expired/stale start, so do not cache or reuse it across accounts. challengeId: type: - string diff --git a/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml index bf934f1ab..98c0728c2 100644 --- a/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml +++ b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml @@ -7,12 +7,21 @@ description: >- the `challengeId`. required: - whitelistedId +anyOf: + - required: + - code + - required: + - passkeyAssertion + - origin properties: whitelistedId: type: string description: >- - The provider beneficiary handle returned as `whitelistedId` by the trust - start, threaded back so confirm need not re-whitelist. + The provider beneficiary handle returned as `whitelistedId` by *this + account's own* trust start, threaded back so confirm need not + re-whitelist. It is scoped to that ceremony — the server rejects a + `whitelistedId` issued for a different external account or from an + expired/stale start, so do not cache or reuse it across accounts. challengeId: type: - string diff --git a/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml index b3804691a..417e31622 100644 --- a/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml +++ b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml @@ -8,5 +8,11 @@ required: properties: eventType: type: string - description: The provider-defined event type to record. + description: >- + A provider-defined security-event identifier drawn from the provider's + accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is + forwarded verbatim to the provider's risk engine, which rejects values + outside that set. Left as a string (rather than an enum) so new + provider-defined events don't require a spec change; treat it as a closed + vocabulary defined by the provider, not free-form input. example: LOGIN diff --git a/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml b/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml index 2d2ddefea..cdc1440ca 100644 --- a/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml +++ b/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml @@ -6,6 +6,12 @@ description: >- 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 diff --git a/openapi/components/schemas/sca/TwoFactorResetStart.yaml b/openapi/components/schemas/sca/TwoFactorResetStart.yaml index 8e7f099aa..874b4946a 100644 --- a/openapi/components/schemas/sca/TwoFactorResetStart.yaml +++ b/openapi/components/schemas/sca/TwoFactorResetStart.yaml @@ -2,8 +2,8 @@ type: object description: >- The reset handle plus the opaque provider 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). `sumsubAccessToken` and - `verificationLink` are omitted when the provider does not return them. + ceremony together (status and complete reference it). `livenessAccessToken` + and `verificationLink` are omitted when the provider does not return them. required: - resetId properties: @@ -11,13 +11,13 @@ properties: type: string description: >- Identifier for this reset; pass it to the status and complete endpoints. - sumsubAccessToken: + livenessAccessToken: type: - string - 'null' description: >- - Access token for the embedded liveness/verification SDK, bound to this - reset. Omitted when the provider does not return one. + Provider-neutral access token for the embedded liveness/verification SDK, + bound to this reset. Omitted when the provider does not return one. verificationLink: type: - string diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml index ccd682e9d..fa039b06d 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml @@ -11,7 +11,7 @@ post: description: | Begin recovering a lost enrolled factor via a liveness-gated, poll-based flow. Opens the provider's liveness check and returns a `resetId` plus the - opaque liveness handles (`sumsubAccessToken` / `verificationLink`) the end + opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end user completes it with. Poll `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness passes, then call the complete endpoint. From 3c53068812c2a07b52e27f5006a7a2246333aad0 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 13 Jul 2026 23:26:55 -0700 Subject: [PATCH 04/10] docs(sca): reframe SCA requirement by region on the management surface Match the #558 terminology fix: the enrollment / login / trust / reset endpoints now say "customers in a region where SCA is required (e.g. EU)" / "customers outside SCA-regulated regions" instead of "customers whose payment provider requires SCA", and the 409 reads "SCA is not required for this customer". Incidental references to the underlying provider as a system (provider-reported status, risk engine, whitelist handle) are left as-is. Co-Authored-By: Claude --- mintlify/openapi.yaml | 90 +++++++------------ openapi.yaml | 90 +++++++------------ ...al-accounts_{externalAccountId}_trust.yaml | 6 +- ...nts_{externalAccountId}_trust_confirm.yaml | 6 +- ...s_{externalAccountId}_untrust_confirm.yaml | 6 +- .../customers_{customerId}_sca_factors.yaml | 6 +- ...mers_{customerId}_sca_factors_passkey.yaml | 6 +- ...stomerId}_sca_factors_passkey_confirm.yaml | 6 +- ...d}_sca_factors_passkey_{credentialId}.yaml | 6 +- ...tomers_{customerId}_sca_factors_reset.yaml | 6 +- ...stomerId}_sca_factors_reset_{resetId}.yaml | 6 +- ..._sca_factors_reset_{resetId}_complete.yaml | 6 +- ...stomers_{customerId}_sca_factors_totp.yaml | 6 +- ...{customerId}_sca_factors_totp_confirm.yaml | 6 +- ...omers_{customerId}_sca_login_complete.yaml | 6 +- ...ustomers_{customerId}_sca_login_start.yaml | 6 +- ...stomers_{customerId}_sca_record-event.yaml | 6 +- 17 files changed, 90 insertions(+), 180 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index cc47de2f7..01d4495ff 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -1141,9 +1141,7 @@ paths: description: | List the Strong Customer Authentication factors the customer has enrolled. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1169,7 +1167,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1197,9 +1195,7 @@ paths: URI; the customer scans it into an authenticator app and confirms with the first code via `POST /customers/{customerId}/sca/factors/totp/confirm`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: startTotpFactorEnrollment tags: - Strong Customer Authentication @@ -1231,7 +1227,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1258,9 +1254,7 @@ paths: start call and the first code the customer's authenticator app produces. Returns one-time recovery codes shown to the customer only once. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 code is always `123456`. operationId: confirmTotpFactorEnrollment @@ -1300,7 +1294,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1328,9 +1322,7 @@ paths: submit the resulting credential via `POST /customers/{customerId}/sca/factors/passkey/confirm`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: startPasskeyFactorEnrollment tags: - Strong Customer Authentication @@ -1362,7 +1354,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1389,9 +1381,7 @@ paths: device produced for the registration challenge, along with the origin it was produced against. Returns the enrolled factor. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: confirmPasskeyFactorEnrollment tags: - Strong Customer Authentication @@ -1429,7 +1419,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1460,9 +1450,7 @@ paths: description: | Delete an enrolled WebAuthn passkey factor by its credential id. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: deletePasskeyFactor tags: - Strong Customer Authentication @@ -1484,7 +1472,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1515,9 +1503,7 @@ paths: returns WebAuthn `passkeyOptions`. Complete with `POST /customers/{customerId}/sca/login/complete`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1555,7 +1541,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1583,9 +1569,7 @@ paths: `PASSKEY`), echoing the `challengeId` for `SMS_OTP`. Returns the provider-reported session status. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1625,7 +1609,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1652,9 +1636,7 @@ paths: underlying provider's risk engine (e.g. a sign-in, a sensitive view), to feed adaptive-authentication signals. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1688,7 +1670,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1718,9 +1700,7 @@ paths: `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness passes, then call the complete endpoint. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1758,7 +1738,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1790,9 +1770,7 @@ paths: Poll the status of an in-progress 2FA reset until it reaches the provider's liveness-passed value, after which the reset can be completed. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1818,7 +1796,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1850,9 +1828,7 @@ paths: Complete a 2FA reset once liveness has passed, clearing the lost factor so the customer can re-enroll. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1880,7 +1856,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1915,9 +1891,7 @@ paths: `scaChallenge` to satisfy. Complete with `POST /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1949,7 +1923,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1983,9 +1957,7 @@ paths: `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when one was issued. Returns `trusted: true`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -2025,7 +1997,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -2059,9 +2031,7 @@ paths: `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when one was issued. Returns `trusted: false`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -2101,7 +2071,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi.yaml b/openapi.yaml index cc47de2f7..01d4495ff 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1141,9 +1141,7 @@ paths: description: | List the Strong Customer Authentication factors the customer has enrolled. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1169,7 +1167,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1197,9 +1195,7 @@ paths: URI; the customer scans it into an authenticator app and confirms with the first code via `POST /customers/{customerId}/sca/factors/totp/confirm`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: startTotpFactorEnrollment tags: - Strong Customer Authentication @@ -1231,7 +1227,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1258,9 +1254,7 @@ paths: start call and the first code the customer's authenticator app produces. Returns one-time recovery codes shown to the customer only once. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 code is always `123456`. operationId: confirmTotpFactorEnrollment @@ -1300,7 +1294,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1328,9 +1322,7 @@ paths: submit the resulting credential via `POST /customers/{customerId}/sca/factors/passkey/confirm`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: startPasskeyFactorEnrollment tags: - Strong Customer Authentication @@ -1362,7 +1354,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1389,9 +1381,7 @@ paths: device produced for the registration challenge, along with the origin it was produced against. Returns the enrolled factor. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: confirmPasskeyFactorEnrollment tags: - Strong Customer Authentication @@ -1429,7 +1419,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1460,9 +1450,7 @@ paths: description: | Delete an enrolled WebAuthn passkey factor by its credential id. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: deletePasskeyFactor tags: - Strong Customer Authentication @@ -1484,7 +1472,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1515,9 +1503,7 @@ paths: returns WebAuthn `passkeyOptions`. Complete with `POST /customers/{customerId}/sca/login/complete`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1555,7 +1541,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1583,9 +1569,7 @@ paths: `PASSKEY`), echoing the `challengeId` for `SMS_OTP`. Returns the provider-reported session status. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1625,7 +1609,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1652,9 +1636,7 @@ paths: underlying provider's risk engine (e.g. a sign-in, a sensitive view), to feed adaptive-authentication signals. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1688,7 +1670,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1718,9 +1700,7 @@ paths: `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness passes, then call the complete endpoint. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1758,7 +1738,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1790,9 +1770,7 @@ paths: Poll the status of an in-progress 2FA reset until it reaches the provider's liveness-passed value, after which the reset can be completed. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1818,7 +1796,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1850,9 +1828,7 @@ paths: Complete a 2FA reset once liveness has passed, clearing the lost factor so the customer can re-enroll. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1880,7 +1856,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1915,9 +1891,7 @@ paths: `scaChallenge` to satisfy. Complete with `POST /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -1949,7 +1923,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -1983,9 +1957,7 @@ paths: `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when one was issued. Returns `trusted: true`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -2025,7 +1997,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: @@ -2059,9 +2031,7 @@ paths: `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when one was issued. Returns `trusted: false`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -2101,7 +2071,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml index c95abb320..d109b9ec9 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml @@ -21,9 +21,7 @@ post: `scaChallenge` to satisfy. Complete with `POST /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -55,7 +53,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml index 33419aad4..5ff7a3140 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml @@ -20,9 +20,7 @@ post: `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when one was issued. Returns `trusted: true`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -62,7 +60,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml index ad10c74bd..f2991d152 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml @@ -20,9 +20,7 @@ post: `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when one was issued. Returns `trusted: false`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -62,7 +60,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors.yaml index 5e1b1eecd..fc8983df6 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors.yaml @@ -11,9 +11,7 @@ get: description: | List the Strong Customer Authentication factors the customer has enrolled. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -39,7 +37,7 @@ get: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml index 7f82a1dd4..bed3961d5 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml @@ -14,9 +14,7 @@ post: submit the resulting credential via `POST /customers/{customerId}/sca/factors/passkey/confirm`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: startPasskeyFactorEnrollment tags: - Strong Customer Authentication @@ -48,7 +46,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml index 9842acaa2..f4917e08b 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml @@ -13,9 +13,7 @@ post: device produced for the registration challenge, along with the origin it was produced against. Returns the enrolled factor. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: confirmPasskeyFactorEnrollment tags: - Strong Customer Authentication @@ -53,7 +51,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml index 101823d9d..20ee7a133 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml @@ -17,9 +17,7 @@ delete: description: | Delete an enrolled WebAuthn passkey factor by its credential id. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: deletePasskeyFactor tags: - Strong Customer Authentication @@ -41,7 +39,7 @@ delete: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml index fa039b06d..1ec0876d9 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml @@ -16,9 +16,7 @@ post: `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness passes, then call the complete endpoint. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -56,7 +54,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml index 7261d949f..f81d74a4a 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml @@ -18,9 +18,7 @@ get: Poll the status of an in-progress 2FA reset until it reaches the provider's liveness-passed value, after which the reset can be completed. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -46,7 +44,7 @@ get: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml index 1fa43be1b..9d05c11ca 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml @@ -18,9 +18,7 @@ post: Complete a 2FA reset once liveness has passed, clearing the lost factor so the customer can re-enroll. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -48,7 +46,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml index 87de36893..bd33420a9 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml @@ -14,9 +14,7 @@ post: URI; the customer scans it into an authenticator app and confirms with the first code via `POST /customers/{customerId}/sca/factors/totp/confirm`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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: startTotpFactorEnrollment tags: - Strong Customer Authentication @@ -48,7 +46,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml index 6c6c87c6e..d41d72bc8 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml @@ -13,9 +13,7 @@ post: start call and the first code the customer's authenticator app produces. Returns one-time recovery codes shown to the customer only once. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 code is always `123456`. operationId: confirmTotpFactorEnrollment @@ -55,7 +53,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml b/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml index 82274a112..652c31f27 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml @@ -14,9 +14,7 @@ post: `PASSKEY`), echoing the `challengeId` for `SMS_OTP`. Returns the provider-reported session status. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -56,7 +54,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_login_start.yaml b/openapi/paths/customers/customers_{customerId}_sca_login_start.yaml index 2c1be8a0e..81864e8a6 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_login_start.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_login_start.yaml @@ -17,9 +17,7 @@ post: returns WebAuthn `passkeyOptions`. Complete with `POST /customers/{customerId}/sca/login/complete`. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -57,7 +55,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml b/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml index ac573c2bc..d3aa14490 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml @@ -13,9 +13,7 @@ post: underlying provider's risk engine (e.g. a sign-in, a sensitive view), to feed adaptive-authentication signals. - This endpoint is only meaningful for customers whose payment provider - requires SCA (e.g. EU customers). For customers whose provider has no such - requirement, this returns `409`. + 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 @@ -49,7 +47,7 @@ post: schema: $ref: ../../components/schemas/errors/Error404.yaml '409': - description: The customer's payment provider does not require SCA. + description: SCA is not required for this customer. content: application/json: schema: From f88e2ef916105596df6d7e70ca318cb1084476ff Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 13 Jul 2026 23:36:56 -0700 Subject: [PATCH 05/10] docs(sca): remove all remaining 'provider' mentions from the management surface To the integrator, Grid is the provider; the underlying provider must never leak into the docs. Reworded provider-reported status -> the (reported) status, the provider's risk engine -> Grid's risk engine, whitelist/liveness handles and 'when the provider does not return one' -> neutral phrasing. Co-Authored-By: Claude --- mintlify/openapi.yaml | 40 +++++++++---------- openapi.yaml | 40 +++++++++---------- .../sca/BeneficiaryTrustConfirmRequest.yaml | 2 +- .../schemas/sca/BeneficiaryTrustStart.yaml | 10 ++--- .../sca/RecordSecurityEventRequest.yaml | 13 +++--- .../schemas/sca/ScaLoginComplete.yaml | 6 +-- .../schemas/sca/TwoFactorResetStart.yaml | 13 +++--- .../schemas/sca/TwoFactorResetStatus.yaml | 10 ++--- ...al-accounts_{externalAccountId}_trust.yaml | 2 +- ...tomers_{customerId}_sca_factors_reset.yaml | 2 +- ...stomerId}_sca_factors_reset_{resetId}.yaml | 3 +- ...omers_{customerId}_sca_login_complete.yaml | 2 +- ...stomers_{customerId}_sca_record-event.yaml | 3 +- 13 files changed, 69 insertions(+), 77 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 01d4495ff..4017affa0 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -1567,7 +1567,7 @@ paths: 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 - provider-reported session status. + 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`. @@ -1632,8 +1632,7 @@ paths: post: summary: Record a security event description: | - Record a client-side security-relevant event for the customer with the - underlying provider's risk engine (e.g. a sign-in, a sensitive view), to + 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`. @@ -1694,7 +1693,7 @@ paths: summary: Start a 2FA reset description: | Begin recovering a lost enrolled factor via a liveness-gated, poll-based - flow. Opens the provider's liveness check and returns a `resetId` plus the + flow. Opens the liveness check and returns a `resetId` plus the opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end user completes it with. Poll `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness @@ -1767,8 +1766,7 @@ paths: get: summary: Get 2FA reset status description: | - Poll the status of an in-progress 2FA reset until it reaches the provider's - liveness-passed value, after which the reset can be completed. + 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 @@ -1887,7 +1885,7 @@ paths: description: | Begin trusting (whitelisting) an external account so future sends to it can skip the per-transaction SCA ceremony. Creates the whitelist entry and - returns a `whitelistedId` handle plus, when the provider issues one, the + returns a `whitelistedId` handle plus, when one is issued, the `scaChallenge` to satisfy. Complete with `POST /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. @@ -12148,23 +12146,23 @@ components: example: https://app.example.com ScaLoginComplete: type: object - description: The provider-reported status of a completed SCA login session. + description: The status of a completed SCA login session. required: - status properties: status: type: string - description: The provider-reported status of the login session, passed through verbatim (Grid does not normalize it). A successful login reports `SUCCESS`; other provider-specific values indicate the login did not complete and should be surfaced to the caller. + 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 RecordSecurityEventRequest: type: object - description: Records a client-side security-relevant event for the customer with the underlying provider's risk engine (e.g. a sign-in, a sensitive view). Used to feed the provider's adaptive-authentication signals. + description: Records a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view). Used to feed Grid's adaptive-authentication signals. required: - eventType properties: eventType: type: string - description: A provider-defined security-event identifier drawn from the provider's accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is forwarded verbatim to the provider's risk engine, which rejects values outside that set. Left as a string (rather than an enum) so new provider-defined events don't require a spec change; treat it as a closed vocabulary defined by the provider, not free-form input. + description: A defined security-event identifier drawn from Grid's accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is forwarded verbatim to Grid's risk engine, which rejects values outside that set. Left as a string (rather than an enum) so new new events don't require a spec change; treat it as a closed vocabulary defined by Grid, not free-form input. example: LOGIN TwoFactorResetStartRequest: type: object @@ -12177,7 +12175,7 @@ components: description: The enrolled factor to reset. TwoFactorResetStart: type: object - description: The reset handle plus the opaque provider 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 the provider does not return them. + 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: @@ -12188,28 +12186,28 @@ components: type: - string - 'null' - description: Provider-neutral access token for the embedded liveness/verification SDK, bound to this reset. Omitted when the provider does not return one. + 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 the provider does not return one. + 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 the provider does not return one. + description: Absolute UTC timestamp at the end of the reset window. Omitted when one is not returned. example: '2025-10-03T12:30:00Z' TwoFactorResetStatus: type: object - description: The provider-reported status of an in-progress 2FA reset, polled until it reaches the provider's liveness-passed value. + description: The status of an in-progress 2FA reset, polled until it reaches the liveness-passed value. required: - status properties: status: type: string - description: The provider-reported status of the reset, passed through verbatim (Grid does not normalize it). Keep polling while it reports a non-terminal value such as `PENDING`; once it reaches the liveness-passed value (`LIVENESS_PASSED`), call the complete endpoint. Other provider-specific values may appear, so treat anything other than the liveness-passed sentinel as "not ready yet" rather than failing. + description: The status of the reset, passed through verbatim (Grid does not normalize it). Keep polling while it reports a non-terminal value such as `PENDING`; once it reaches the liveness-passed value (`LIVENESS_PASSED`), call the complete endpoint. Other values may appear, so treat anything other than the liveness-passed sentinel as "not ready yet" rather than failing. example: PENDING ScaChallenge: type: object @@ -12267,16 +12265,16 @@ components: - https://app.example.com BeneficiaryTrustStart: type: object - description: The provider handle plus the SCA challenge a caller authorizes to finish trusting (or untrusting) a beneficiary. `whitelistedId` is the provider's beneficiary handle; thread it back on the confirm call so confirm need not re-whitelist (which could trigger a second challenge). `scaChallenge` is omitted when the provider does not surface one on the whitelist response; the caller then confirms without a `challengeId`. + description: The whitelist handle plus the SCA challenge a caller authorizes to finish trusting (or untrusting) a beneficiary. `whitelistedId` is the beneficiary handle; thread it back on the confirm call so confirm need not re-whitelist (which could trigger a second challenge). `scaChallenge` is omitted when one is not surfaced on the whitelist response; the caller then confirms without a `challengeId`. required: - whitelistedId properties: whitelistedId: type: string - description: The provider's beneficiary (whitelist) handle. Thread it back on the confirm call. + description: The beneficiary (whitelist) handle. Thread it back on the confirm call. scaChallenge: $ref: '#/components/schemas/ScaChallenge' - description: The SCA challenge to satisfy on the confirm call. Omitted when the provider issues no challenge on the whitelist response. + description: The SCA challenge to satisfy on the confirm call. Omitted when the no challenge is issued on the whitelist response. 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`), plus the `whitelistedId` returned by the trust start and, when the start issued one, the `challengeId`. @@ -12291,7 +12289,7 @@ components: properties: whitelistedId: type: string - description: The provider beneficiary handle returned as `whitelistedId` by *this account's own* trust start, threaded back so confirm need not re-whitelist. It is scoped to that ceremony — the server rejects a `whitelistedId` issued for a different external account or from an expired/stale start, so do not cache or reuse it across accounts. + description: The beneficiary handle returned as `whitelistedId` by *this account's own* trust start, threaded back so confirm need not re-whitelist. It is scoped to that ceremony — the server rejects a `whitelistedId` issued for a different external account or from an expired/stale start, so do not cache or reuse it across accounts. challengeId: type: - string diff --git a/openapi.yaml b/openapi.yaml index 01d4495ff..4017affa0 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1567,7 +1567,7 @@ paths: 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 - provider-reported session status. + 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`. @@ -1632,8 +1632,7 @@ paths: post: summary: Record a security event description: | - Record a client-side security-relevant event for the customer with the - underlying provider's risk engine (e.g. a sign-in, a sensitive view), to + 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`. @@ -1694,7 +1693,7 @@ paths: summary: Start a 2FA reset description: | Begin recovering a lost enrolled factor via a liveness-gated, poll-based - flow. Opens the provider's liveness check and returns a `resetId` plus the + flow. Opens the liveness check and returns a `resetId` plus the opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end user completes it with. Poll `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness @@ -1767,8 +1766,7 @@ paths: get: summary: Get 2FA reset status description: | - Poll the status of an in-progress 2FA reset until it reaches the provider's - liveness-passed value, after which the reset can be completed. + 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 @@ -1887,7 +1885,7 @@ paths: description: | Begin trusting (whitelisting) an external account so future sends to it can skip the per-transaction SCA ceremony. Creates the whitelist entry and - returns a `whitelistedId` handle plus, when the provider issues one, the + returns a `whitelistedId` handle plus, when one is issued, the `scaChallenge` to satisfy. Complete with `POST /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. @@ -12148,23 +12146,23 @@ components: example: https://app.example.com ScaLoginComplete: type: object - description: The provider-reported status of a completed SCA login session. + description: The status of a completed SCA login session. required: - status properties: status: type: string - description: The provider-reported status of the login session, passed through verbatim (Grid does not normalize it). A successful login reports `SUCCESS`; other provider-specific values indicate the login did not complete and should be surfaced to the caller. + 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 RecordSecurityEventRequest: type: object - description: Records a client-side security-relevant event for the customer with the underlying provider's risk engine (e.g. a sign-in, a sensitive view). Used to feed the provider's adaptive-authentication signals. + description: Records a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view). Used to feed Grid's adaptive-authentication signals. required: - eventType properties: eventType: type: string - description: A provider-defined security-event identifier drawn from the provider's accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is forwarded verbatim to the provider's risk engine, which rejects values outside that set. Left as a string (rather than an enum) so new provider-defined events don't require a spec change; treat it as a closed vocabulary defined by the provider, not free-form input. + description: A defined security-event identifier drawn from Grid's accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is forwarded verbatim to Grid's risk engine, which rejects values outside that set. Left as a string (rather than an enum) so new new events don't require a spec change; treat it as a closed vocabulary defined by Grid, not free-form input. example: LOGIN TwoFactorResetStartRequest: type: object @@ -12177,7 +12175,7 @@ components: description: The enrolled factor to reset. TwoFactorResetStart: type: object - description: The reset handle plus the opaque provider 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 the provider does not return them. + 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: @@ -12188,28 +12186,28 @@ components: type: - string - 'null' - description: Provider-neutral access token for the embedded liveness/verification SDK, bound to this reset. Omitted when the provider does not return one. + 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 the provider does not return one. + 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 the provider does not return one. + description: Absolute UTC timestamp at the end of the reset window. Omitted when one is not returned. example: '2025-10-03T12:30:00Z' TwoFactorResetStatus: type: object - description: The provider-reported status of an in-progress 2FA reset, polled until it reaches the provider's liveness-passed value. + description: The status of an in-progress 2FA reset, polled until it reaches the liveness-passed value. required: - status properties: status: type: string - description: The provider-reported status of the reset, passed through verbatim (Grid does not normalize it). Keep polling while it reports a non-terminal value such as `PENDING`; once it reaches the liveness-passed value (`LIVENESS_PASSED`), call the complete endpoint. Other provider-specific values may appear, so treat anything other than the liveness-passed sentinel as "not ready yet" rather than failing. + description: The status of the reset, passed through verbatim (Grid does not normalize it). Keep polling while it reports a non-terminal value such as `PENDING`; once it reaches the liveness-passed value (`LIVENESS_PASSED`), call the complete endpoint. Other values may appear, so treat anything other than the liveness-passed sentinel as "not ready yet" rather than failing. example: PENDING ScaChallenge: type: object @@ -12267,16 +12265,16 @@ components: - https://app.example.com BeneficiaryTrustStart: type: object - description: The provider handle plus the SCA challenge a caller authorizes to finish trusting (or untrusting) a beneficiary. `whitelistedId` is the provider's beneficiary handle; thread it back on the confirm call so confirm need not re-whitelist (which could trigger a second challenge). `scaChallenge` is omitted when the provider does not surface one on the whitelist response; the caller then confirms without a `challengeId`. + description: The whitelist handle plus the SCA challenge a caller authorizes to finish trusting (or untrusting) a beneficiary. `whitelistedId` is the beneficiary handle; thread it back on the confirm call so confirm need not re-whitelist (which could trigger a second challenge). `scaChallenge` is omitted when one is not surfaced on the whitelist response; the caller then confirms without a `challengeId`. required: - whitelistedId properties: whitelistedId: type: string - description: The provider's beneficiary (whitelist) handle. Thread it back on the confirm call. + description: The beneficiary (whitelist) handle. Thread it back on the confirm call. scaChallenge: $ref: '#/components/schemas/ScaChallenge' - description: The SCA challenge to satisfy on the confirm call. Omitted when the provider issues no challenge on the whitelist response. + description: The SCA challenge to satisfy on the confirm call. Omitted when the no challenge is issued on the whitelist response. 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`), plus the `whitelistedId` returned by the trust start and, when the start issued one, the `challengeId`. @@ -12291,7 +12289,7 @@ components: properties: whitelistedId: type: string - description: The provider beneficiary handle returned as `whitelistedId` by *this account's own* trust start, threaded back so confirm need not re-whitelist. It is scoped to that ceremony — the server rejects a `whitelistedId` issued for a different external account or from an expired/stale start, so do not cache or reuse it across accounts. + description: The beneficiary handle returned as `whitelistedId` by *this account's own* trust start, threaded back so confirm need not re-whitelist. It is scoped to that ceremony — the server rejects a `whitelistedId` issued for a different external account or from an expired/stale start, so do not cache or reuse it across accounts. challengeId: type: - string diff --git a/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml index 98c0728c2..bab52e49a 100644 --- a/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml +++ b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml @@ -17,7 +17,7 @@ properties: whitelistedId: type: string description: >- - The provider beneficiary handle returned as `whitelistedId` by *this + The beneficiary handle returned as `whitelistedId` by *this account's own* trust start, threaded back so confirm need not re-whitelist. It is scoped to that ceremony — the server rejects a `whitelistedId` issued for a different external account or from an diff --git a/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml b/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml index 389574c70..32982b25a 100644 --- a/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml +++ b/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml @@ -1,10 +1,10 @@ type: object description: >- - The provider handle plus the SCA challenge a caller authorizes to finish - trusting (or untrusting) a beneficiary. `whitelistedId` is the provider's + The whitelist handle plus the SCA challenge a caller authorizes to finish + trusting (or untrusting) a beneficiary. `whitelistedId` is the beneficiary handle; thread it back on the confirm call so confirm need not re-whitelist (which could trigger a second challenge). `scaChallenge` is - omitted when the provider does not surface one on the whitelist response; the + omitted when one is not surfaced on the whitelist response; the caller then confirms without a `challengeId`. required: - whitelistedId @@ -12,10 +12,10 @@ properties: whitelistedId: type: string description: >- - The provider's beneficiary (whitelist) handle. Thread it back on the + The beneficiary (whitelist) handle. Thread it back on the confirm call. scaChallenge: $ref: ./ScaChallenge.yaml description: >- The SCA challenge to satisfy on the confirm call. Omitted when the - provider issues no challenge on the whitelist response. + no challenge is issued on the whitelist response. diff --git a/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml index 417e31622..95426c66a 100644 --- a/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml +++ b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml @@ -1,18 +1,17 @@ type: object description: >- - Records a client-side security-relevant event for the customer with the - underlying provider's risk engine (e.g. a sign-in, a sensitive view). Used to - feed the provider's adaptive-authentication signals. + Records a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view). Used to + feed Grid's adaptive-authentication signals. required: - eventType properties: eventType: type: string description: >- - A provider-defined security-event identifier drawn from the provider's + A defined security-event identifier drawn from Grid's accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is - forwarded verbatim to the provider's risk engine, which rejects values + forwarded verbatim to Grid's risk engine, which rejects values outside that set. Left as a string (rather than an enum) so new - provider-defined events don't require a spec change; treat it as a closed - vocabulary defined by the provider, not free-form input. + new events don't require a spec change; treat it as a closed + vocabulary defined by Grid, not free-form input. example: LOGIN diff --git a/openapi/components/schemas/sca/ScaLoginComplete.yaml b/openapi/components/schemas/sca/ScaLoginComplete.yaml index f82f642d5..e81240c45 100644 --- a/openapi/components/schemas/sca/ScaLoginComplete.yaml +++ b/openapi/components/schemas/sca/ScaLoginComplete.yaml @@ -1,13 +1,13 @@ type: object -description: The provider-reported status of a completed SCA login session. +description: The status of a completed SCA login session. required: - status properties: status: type: string description: >- - The provider-reported status of the login session, passed through + The status of the login session, passed through verbatim (Grid does not normalize it). A successful login reports - `SUCCESS`; other provider-specific values indicate the login did not + `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/TwoFactorResetStart.yaml b/openapi/components/schemas/sca/TwoFactorResetStart.yaml index 874b4946a..e0183c819 100644 --- a/openapi/components/schemas/sca/TwoFactorResetStart.yaml +++ b/openapi/components/schemas/sca/TwoFactorResetStart.yaml @@ -1,9 +1,9 @@ type: object description: >- - The reset handle plus the opaque provider liveness handles a caller relays to + 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 the provider does not return them. + and `verificationLink` are omitted when they are not returned. required: - resetId properties: @@ -16,21 +16,20 @@ properties: - string - 'null' description: >- - Provider-neutral access token for the embedded liveness/verification SDK, - bound to this reset. Omitted when the provider does not return one. + 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 the provider does not return one. + 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 the - provider does not return one. + 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/TwoFactorResetStatus.yaml b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml index ac92f73da..ae9e35212 100644 --- a/openapi/components/schemas/sca/TwoFactorResetStatus.yaml +++ b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml @@ -1,17 +1,17 @@ type: object description: >- - The provider-reported status of an in-progress 2FA reset, polled until it - reaches the provider's liveness-passed value. + The status of an in-progress 2FA reset, polled until it + reaches the liveness-passed value. required: - status properties: status: type: string description: >- - The provider-reported status of the reset, passed through verbatim (Grid + The status of the reset, passed through verbatim (Grid does not normalize it). Keep polling while it reports a non-terminal value such as `PENDING`; once it reaches the liveness-passed value - (`LIVENESS_PASSED`), call the complete endpoint. Other provider-specific - values may appear, so treat anything other than the liveness-passed + (`LIVENESS_PASSED`), call the complete endpoint. Other values may appear, + so treat anything other than the liveness-passed sentinel as "not ready yet" rather than failing. example: PENDING diff --git a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml index d109b9ec9..471678076 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml @@ -17,7 +17,7 @@ post: description: | Begin trusting (whitelisting) an external account so future sends to it can skip the per-transaction SCA ceremony. Creates the whitelist entry and - returns a `whitelistedId` handle plus, when the provider issues one, the + returns a `whitelistedId` handle plus, when one is issued, the `scaChallenge` to satisfy. Complete with `POST /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml index 1ec0876d9..8bf22c2cb 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml @@ -10,7 +10,7 @@ post: summary: Start a 2FA reset description: | Begin recovering a lost enrolled factor via a liveness-gated, poll-based - flow. Opens the provider's liveness check and returns a `resetId` plus the + flow. Opens the liveness check and returns a `resetId` plus the opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end user completes it with. Poll `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml index f81d74a4a..9f70e1284 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml @@ -15,8 +15,7 @@ parameters: get: summary: Get 2FA reset status description: | - Poll the status of an in-progress 2FA reset until it reaches the provider's - liveness-passed value, after which the reset can be completed. + 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 diff --git a/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml b/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml index 652c31f27..08d565c84 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml @@ -12,7 +12,7 @@ post: 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 - provider-reported session status. + 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`. diff --git a/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml b/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml index d3aa14490..473fb5756 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml +++ b/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml @@ -9,8 +9,7 @@ parameters: post: summary: Record a security event description: | - Record a client-side security-relevant event for the customer with the - underlying provider's risk engine (e.g. a sign-in, a sensitive view), to + 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`. From f8f8fb629ad9cf7dd752fb2b32a6c7baf42faa6f Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 13 Jul 2026 23:55:37 -0700 Subject: [PATCH 06/10] docs(sca): enumerate record-event values; abstract away whitelistedId Per review on #600: - RecordSecurityEventRequest.eventType is now an explicit enum (RESET_PASSWORD_COMPLETED | FAILED_LOGIN_ATTEMPT) with each event's effect, instead of a vague free-string "closed vocabulary". - Drop whitelistedId from the beneficiary-trust surface: BeneficiaryTrustStart returns just the scaChallenge, and confirm/untrust identify the beneficiary by the {externalAccountId} already in the path. Requires the backend to persist the externalAccountId -> whitelist handle mapping (tracked follow-up). Co-Authored-By: Claude --- mintlify/openapi.yaml | 46 +++++++++---------- openapi.yaml | 46 +++++++++---------- .../sca/BeneficiaryTrustConfirmRequest.yaml | 16 ++----- .../schemas/sca/BeneficiaryTrustStart.yaml | 22 +++------ .../sca/RecordSecurityEventRequest.yaml | 24 ++++++---- ...al-accounts_{externalAccountId}_trust.yaml | 7 ++- ...nts_{externalAccountId}_trust_confirm.yaml | 4 +- ...s_{externalAccountId}_untrust_confirm.yaml | 4 +- 8 files changed, 75 insertions(+), 94 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 4017affa0..29a75af46 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -1884,9 +1884,8 @@ paths: summary: Start trusting a beneficiary description: | Begin trusting (whitelisting) an external account so future sends to it can - skip the per-transaction SCA ceremony. Creates the whitelist entry and - returns a `whitelistedId` handle plus, when one is issued, the - `scaChallenge` to satisfy. Complete with + skip the per-transaction SCA ceremony. Returns the `scaChallenge` to satisfy + (when one is issued). Complete with `POST /customers/{customerId}/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`. @@ -1897,7 +1896,7 @@ paths: - BasicAuth: [] responses: '200': - description: Beneficiary trust started; the whitelist handle and any challenge are returned. + description: Beneficiary trust started; the SCA challenge (if any) is returned. content: application/json: schema: @@ -1950,8 +1949,8 @@ paths: post: summary: Confirm trusting a beneficiary description: | - Finalize trusting a beneficiary by submitting the `whitelistedId` from the - trust start and the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + 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`. @@ -2024,8 +2023,8 @@ paths: post: summary: Confirm untrusting a beneficiary description: | - Finalize untrusting a beneficiary by submitting the `whitelistedId` from the - trust start and the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + 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`. @@ -12156,14 +12155,23 @@ components: example: SUCCESS RecordSecurityEventRequest: type: object - description: Records a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view). Used to feed Grid's adaptive-authentication signals. + 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 - description: A defined security-event identifier drawn from Grid's accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is forwarded verbatim to Grid's risk engine, which rejects values outside that set. Left as a string (rather than an enum) so new new events don't require a spec change; treat it as a closed vocabulary defined by Grid, not free-form input. - example: LOGIN + 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 TwoFactorResetStartRequest: type: object description: Selects which enrolled factor to reset via the liveness-gated recovery flow. @@ -12265,21 +12273,14 @@ components: - https://app.example.com BeneficiaryTrustStart: type: object - description: The whitelist handle plus the SCA challenge a caller authorizes to finish trusting (or untrusting) a beneficiary. `whitelistedId` is the beneficiary handle; thread it back on the confirm call so confirm need not re-whitelist (which could trigger a second challenge). `scaChallenge` is omitted when one is not surfaced on the whitelist response; the caller then confirms without a `challengeId`. - required: - - whitelistedId + 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: - whitelistedId: - type: string - description: The beneficiary (whitelist) handle. Thread it back on the confirm call. scaChallenge: $ref: '#/components/schemas/ScaChallenge' - description: The SCA challenge to satisfy on the confirm call. Omitted when the no challenge is issued on the whitelist response. + 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`), plus the `whitelistedId` returned by the trust start and, when the start issued one, the `challengeId`. - required: - - whitelistedId + 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 @@ -12287,9 +12288,6 @@ components: - passkeyAssertion - origin properties: - whitelistedId: - type: string - description: The beneficiary handle returned as `whitelistedId` by *this account's own* trust start, threaded back so confirm need not re-whitelist. It is scoped to that ceremony — the server rejects a `whitelistedId` issued for a different external account or from an expired/stale start, so do not cache or reuse it across accounts. challengeId: type: - string diff --git a/openapi.yaml b/openapi.yaml index 4017affa0..29a75af46 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1884,9 +1884,8 @@ paths: summary: Start trusting a beneficiary description: | Begin trusting (whitelisting) an external account so future sends to it can - skip the per-transaction SCA ceremony. Creates the whitelist entry and - returns a `whitelistedId` handle plus, when one is issued, the - `scaChallenge` to satisfy. Complete with + skip the per-transaction SCA ceremony. Returns the `scaChallenge` to satisfy + (when one is issued). Complete with `POST /customers/{customerId}/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`. @@ -1897,7 +1896,7 @@ paths: - BasicAuth: [] responses: '200': - description: Beneficiary trust started; the whitelist handle and any challenge are returned. + description: Beneficiary trust started; the SCA challenge (if any) is returned. content: application/json: schema: @@ -1950,8 +1949,8 @@ paths: post: summary: Confirm trusting a beneficiary description: | - Finalize trusting a beneficiary by submitting the `whitelistedId` from the - trust start and the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + 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`. @@ -2024,8 +2023,8 @@ paths: post: summary: Confirm untrusting a beneficiary description: | - Finalize untrusting a beneficiary by submitting the `whitelistedId` from the - trust start and the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + 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`. @@ -12156,14 +12155,23 @@ components: example: SUCCESS RecordSecurityEventRequest: type: object - description: Records a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view). Used to feed Grid's adaptive-authentication signals. + 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 - description: A defined security-event identifier drawn from Grid's accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is forwarded verbatim to Grid's risk engine, which rejects values outside that set. Left as a string (rather than an enum) so new new events don't require a spec change; treat it as a closed vocabulary defined by Grid, not free-form input. - example: LOGIN + 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 TwoFactorResetStartRequest: type: object description: Selects which enrolled factor to reset via the liveness-gated recovery flow. @@ -12265,21 +12273,14 @@ components: - https://app.example.com BeneficiaryTrustStart: type: object - description: The whitelist handle plus the SCA challenge a caller authorizes to finish trusting (or untrusting) a beneficiary. `whitelistedId` is the beneficiary handle; thread it back on the confirm call so confirm need not re-whitelist (which could trigger a second challenge). `scaChallenge` is omitted when one is not surfaced on the whitelist response; the caller then confirms without a `challengeId`. - required: - - whitelistedId + 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: - whitelistedId: - type: string - description: The beneficiary (whitelist) handle. Thread it back on the confirm call. scaChallenge: $ref: '#/components/schemas/ScaChallenge' - description: The SCA challenge to satisfy on the confirm call. Omitted when the no challenge is issued on the whitelist response. + 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`), plus the `whitelistedId` returned by the trust start and, when the start issued one, the `challengeId`. - required: - - whitelistedId + 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 @@ -12287,9 +12288,6 @@ components: - passkeyAssertion - origin properties: - whitelistedId: - type: string - description: The beneficiary handle returned as `whitelistedId` by *this account's own* trust start, threaded back so confirm need not re-whitelist. It is scoped to that ceremony — the server rejects a `whitelistedId` issued for a different external account or from an expired/stale start, so do not cache or reuse it across accounts. challengeId: type: - string diff --git a/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml index bab52e49a..686da5819 100644 --- a/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml +++ b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml @@ -2,11 +2,9 @@ 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`), plus the - `whitelistedId` returned by the trust start and, when the start issued one, - the `challengeId`. -required: - - whitelistedId + `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 @@ -14,14 +12,6 @@ anyOf: - passkeyAssertion - origin properties: - whitelistedId: - type: string - description: >- - The beneficiary handle returned as `whitelistedId` by *this - account's own* trust start, threaded back so confirm need not - re-whitelist. It is scoped to that ceremony — the server rejects a - `whitelistedId` issued for a different external account or from an - expired/stale start, so do not cache or reuse it across accounts. challengeId: type: - string diff --git a/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml b/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml index 32982b25a..85d2565d6 100644 --- a/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml +++ b/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml @@ -1,21 +1,13 @@ type: object description: >- - The whitelist handle plus the SCA challenge a caller authorizes to finish - trusting (or untrusting) a beneficiary. `whitelistedId` is the - beneficiary handle; thread it back on the confirm call so confirm need not - re-whitelist (which could trigger a second challenge). `scaChallenge` is - omitted when one is not surfaced on the whitelist response; the - caller then confirms without a `challengeId`. -required: - - whitelistedId + 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: - whitelistedId: - type: string - description: >- - The beneficiary (whitelist) handle. Thread it back on the - confirm call. scaChallenge: $ref: ./ScaChallenge.yaml description: >- - The SCA challenge to satisfy on the confirm call. Omitted when the - no challenge is issued on the whitelist response. + The SCA challenge to satisfy on the confirm call. Omitted when no challenge + is issued. diff --git a/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml index 95426c66a..0ba668d80 100644 --- a/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml +++ b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml @@ -1,17 +1,21 @@ type: object description: >- - Records a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view). Used to - feed Grid's adaptive-authentication signals. + 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 - description: >- - A defined security-event identifier drawn from Grid's - accepted set (e.g. `LOGIN`), **not** arbitrary caller text — it is - forwarded verbatim to Grid's risk engine, which rejects values - outside that set. Left as a string (rather than an enum) so new - new events don't require a spec change; treat it as a closed - vocabulary defined by Grid, not free-form input. - example: LOGIN + 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/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml index 471678076..e9abc93ec 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml @@ -16,9 +16,8 @@ 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. Creates the whitelist entry and - returns a `whitelistedId` handle plus, when one is issued, the - `scaChallenge` to satisfy. Complete with + skip the per-transaction SCA ceremony. Returns the `scaChallenge` to satisfy + (when one is issued). Complete with `POST /customers/{customerId}/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`. @@ -29,7 +28,7 @@ post: - BasicAuth: [] responses: '200': - description: Beneficiary trust started; the whitelist handle and any challenge are returned. + description: Beneficiary trust started; the SCA challenge (if any) is returned. content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml index 5ff7a3140..0ad98c8dc 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml @@ -15,8 +15,8 @@ parameters: post: summary: Confirm trusting a beneficiary description: | - Finalize trusting a beneficiary by submitting the `whitelistedId` from the - trust start and the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + 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`. diff --git a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml index f2991d152..70892bcfa 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml +++ b/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml @@ -15,8 +15,8 @@ parameters: post: summary: Confirm untrusting a beneficiary description: | - Finalize untrusting a beneficiary by submitting the `whitelistedId` from the - trust start and the SCA proof (`code` for `SMS_OTP` / `TOTP`, or + 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`. From 7a57accd1711be7468f13b72b31cd12826c7eeca Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Tue, 14 Jul 2026 10:18:52 -0700 Subject: [PATCH 07/10] feat(sca): add startBeneficiaryUntrust endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Untrust previously had only a confirm step, so an SMS_OTP (default, no enrollment) or PASSKEY customer had no way to obtain the SCA challenge that untrust/confirm expects — only a self-served TOTP code worked. Add POST /customers/{customerId}/external-accounts/{externalAccountId}/untrust (startBeneficiaryUntrust), mirroring startBeneficiaryTrust: it issues the scaChallenge that untrust/confirm satisfies. Returns BeneficiaryTrustStart. --- mintlify/openapi.yaml | 767 ++++++++++-------- openapi.yaml | 767 ++++++++++-------- .../components/schemas/errors/Error409.yaml | 2 + .../components/schemas/errors/Error423.yaml | 40 + .../sca/PasskeyEnrollmentConfirmRequest.yaml | 6 + .../sca/PasskeyEnrollmentConfirmResponse.yaml | 6 + .../schemas/sca/PasskeyEnrollmentStart.yaml | 6 + .../sca/PasskeyFactorEnrollRequest.yaml | 11 + .../sca/RecordSecurityEventResponse.yaml | 35 + .../sca/ScaFactorConfirmRequestOneOf.yaml | 11 + .../sca/ScaFactorConfirmResponseOneOf.yaml | 9 + .../sca/ScaFactorEnrollRequestOneOf.yaml | 12 + .../sca/ScaFactorEnrollStartOneOf.yaml | 12 + .../sca/TotpEnrollmentConfirmRequest.yaml | 6 + .../sca/TotpEnrollmentConfirmResponse.yaml | 6 + .../schemas/sca/TotpEnrollmentStart.yaml | 8 +- .../schemas/sca/TotpFactorEnrollRequest.yaml | 11 + .../sca/TwoFactorResetCompleteRequest.yaml | 23 + .../schemas/sca/TwoFactorResetStatus.yaml | 60 +- openapi/openapi.yaml | 56 +- ...l_accounts_{externalAccountId}_trust.yaml} | 9 +- ...ts_{externalAccountId}_trust_confirm.yaml} | 7 - ...accounts_{externalAccountId}_untrust.yaml} | 24 +- ..._{externalAccountId}_untrust_confirm.yaml} | 7 - .../customers_{customerId}_sca_factors.yaml | 50 -- ...mers_{customerId}_sca_factors_passkey.yaml | 59 -- ...stomerId}_sca_factors_passkey_confirm.yaml | 64 -- openapi/paths/sca/sca_factors.yaml | 114 +++ .../sca_factors_confirm.yaml} | 30 +- .../sca_factors_reset.yaml} | 12 +- .../sca_factors_reset_{resetId}.yaml} | 2 +- ...sca_factors_reset_{resetId}_complete.yaml} | 11 +- .../sca_factors_{credentialId}.yaml} | 17 +- .../sca_login_complete.yaml} | 10 +- .../sca_login_start.yaml} | 4 +- .../sca_record-event.yaml} | 20 +- 36 files changed, 1359 insertions(+), 935 deletions(-) create mode 100644 openapi/components/schemas/errors/Error423.yaml create mode 100644 openapi/components/schemas/sca/PasskeyFactorEnrollRequest.yaml create mode 100644 openapi/components/schemas/sca/RecordSecurityEventResponse.yaml create mode 100644 openapi/components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml create mode 100644 openapi/components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml create mode 100644 openapi/components/schemas/sca/ScaFactorEnrollRequestOneOf.yaml create mode 100644 openapi/components/schemas/sca/ScaFactorEnrollStartOneOf.yaml create mode 100644 openapi/components/schemas/sca/TotpFactorEnrollRequest.yaml create mode 100644 openapi/components/schemas/sca/TwoFactorResetCompleteRequest.yaml rename openapi/paths/customers/{customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml => customers_external_accounts_{externalAccountId}_trust.yaml} (85%) rename openapi/paths/customers/{customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml => customers_external_accounts_{externalAccountId}_trust_confirm.yaml} (90%) rename openapi/paths/customers/{customers_{customerId}_sca_factors_totp.yaml => customers_external_accounts_{externalAccountId}_untrust.yaml} (62%) rename openapi/paths/customers/{customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml => customers_external_accounts_{externalAccountId}_untrust_confirm.yaml} (90%) delete mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors.yaml delete mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml delete mode 100644 openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml create mode 100644 openapi/paths/sca/sca_factors.yaml rename openapi/paths/{customers/customers_{customerId}_sca_factors_totp_confirm.yaml => sca/sca_factors_confirm.yaml} (59%) rename openapi/paths/{customers/customers_{customerId}_sca_factors_reset.yaml => sca/sca_factors_reset.yaml} (84%) rename openapi/paths/{customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml => sca/sca_factors_reset_{resetId}.yaml} (99%) rename openapi/paths/{customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml => sca/sca_factors_reset_{resetId}_complete.yaml} (83%) rename openapi/paths/{customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml => sca/sca_factors_{credentialId}.yaml} (69%) rename openapi/paths/{customers/customers_{customerId}_sca_login_complete.yaml => sca/sca_login_complete.yaml} (87%) rename openapi/paths/{customers/customers_{customerId}_sca_login_start.yaml => sca/sca_login_start.yaml} (97%) rename openapi/paths/{customers/customers_{customerId}_sca_record-event.yaml => sca/sca_record-event.yaml} (74%) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 29a75af46..34d5f7061 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -1127,11 +1127,11 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors: + /sca/factors: parameters: - name: customerId - in: path - description: The unique identifier of the customer whose enrolled factors are listed. + in: query + description: The unique identifier of the customer whose factors are listed or enrolled. required: true schema: type: string @@ -1178,86 +1178,20 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors/totp: - parameters: - - name: customerId - in: path - description: The unique identifier of the customer enrolling a TOTP factor. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Start TOTP factor enrollment + summary: Start SCA factor enrollment description: | - Begin enrolling a time-based one-time-password (TOTP) authenticator factor - for the customer. Returns the shared secret and an `otpauth://` provisioning - URI; the customer scans it into an authenticator app and confirms with the - first code via `POST /customers/{customerId}/sca/factors/totp/confirm`. + Begin enrolling an SCA factor for the customer. The request body's `type` + selects the factor — `TOTP` or `PASSKEY`. `SMS_OTP` is not enrollable (it uses + the customer's verified phone). Returns the factor-specific material needed to + finish via `POST /sca/factors/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: startTotpFactorEnrollment - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - responses: - '200': - description: TOTP enrollment started; the shared secret is returned. - content: - application/json: - schema: - $ref: '#/components/schemas/TotpEnrollmentStart' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - 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' - /customers/{customerId}/sca/factors/totp/confirm: - parameters: - - name: customerId - in: path - description: The unique identifier of the customer confirming a TOTP factor. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - post: - summary: Confirm TOTP factor enrollment - description: | - Finalize TOTP factor enrollment by submitting the shared secret from the - start call and the first code the customer's authenticator app produces. - Returns one-time recovery codes shown to the customer only once. + 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`. - - In sandbox, the code is always `123456`. - operationId: confirmTotpFactorEnrollment + operationId: startScaFactorEnrollment tags: - Strong Customer Authentication security: @@ -1267,74 +1201,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TotpEnrollmentConfirmRequest' - responses: - '200': - description: TOTP factor enrolled; recovery codes are returned. - content: - application/json: - schema: - $ref: '#/components/schemas/TotpEnrollmentConfirmResponse' - '400': - description: Invalid or incorrect confirmation code - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - 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' - /customers/{customerId}/sca/factors/passkey: - parameters: - - name: customerId - in: path - description: The unique identifier of the customer enrolling a passkey factor. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - post: - summary: Start passkey factor enrollment - description: | - Begin enrolling a WebAuthn passkey factor for the customer. Returns opaque - WebAuthn registration `options`; pass them to the device's WebAuthn API and - submit the resulting credential via - `POST /customers/{customerId}/sca/factors/passkey/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: startPasskeyFactorEnrollment - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] + $ref: '#/components/schemas/ScaFactorEnrollRequestOneOf' responses: '200': - description: Passkey enrollment started; WebAuthn registration options are returned. + description: Enrollment started; the factor-specific completion material is returned. content: application/json: schema: - $ref: '#/components/schemas/PasskeyEnrollmentStart' + $ref: '#/components/schemas/ScaFactorEnrollStartOneOf' '400': description: Invalid request content: @@ -1354,7 +1228,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: SCA is not required for this customer. + 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: @@ -1365,24 +1239,32 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors/passkey/confirm: + /sca/factors/confirm: parameters: - name: customerId - in: path - description: The unique identifier of the customer confirming a passkey factor. + 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 passkey factor enrollment + summary: Confirm SCA factor enrollment description: | - Finalize passkey factor enrollment by submitting the WebAuthn credential the - device produced for the registration challenge, along with the origin it was - produced against. Returns the enrolled factor. + 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`. - operationId: confirmPasskeyFactorEnrollment + + In sandbox, the TOTP code is always `123456`. + operationId: confirmScaFactorEnrollment tags: - Strong Customer Authentication security: @@ -1392,16 +1274,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PasskeyEnrollmentConfirmRequest' + $ref: '#/components/schemas/ScaFactorConfirmRequestOneOf' responses: '200': - description: Passkey factor enrolled; the enrolled factor is returned. + description: Factor enrolled; the factor-specific result is returned. content: application/json: schema: - $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse' + $ref: '#/components/schemas/ScaFactorConfirmResponseOneOf' '400': - description: Invalid credential or origin + description: Invalid or incorrect confirmation proof content: application/json: schema: @@ -1430,35 +1312,36 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors/passkey/{credentialId}: + /sca/factors/{credentialId}: parameters: - name: customerId - in: path - description: The unique identifier of the customer whose passkey is being deleted. + 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 passkey to delete (from the enrolled factor's `credentialId`). + 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 passkey factor + summary: Delete an enrolled SCA factor description: | - Delete an enrolled WebAuthn passkey factor by its credential id. + 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: deletePasskeyFactor + operationId: deleteScaFactor tags: - Strong Customer Authentication security: - BasicAuth: [] responses: '204': - description: Passkey deleted; no content is returned. + description: Factor deleted; no content is returned. '401': description: Unauthorized content: @@ -1466,7 +1349,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Customer or passkey not found + description: Customer or factor not found content: application/json: schema: @@ -1483,10 +1366,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/login/start: + /sca/login/start: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer starting an SCA login. required: true schema: @@ -1501,7 +1384,7 @@ paths: 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 /customers/{customerId}/sca/login/complete`. + `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 @@ -1552,10 +1435,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/login/complete: + /sca/login/complete: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer completing an SCA login. required: true schema: @@ -1614,16 +1497,22 @@ paths: 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' - /customers/{customerId}/sca/record-event: + /sca/record-event: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer the security event is recorded for. required: true schema: @@ -1648,8 +1537,12 @@ paths: schema: $ref: '#/components/schemas/RecordSecurityEventRequest' responses: - '204': - description: Event recorded; no content is returned. + '200': + description: Event recorded; the customer's resulting login-security state is returned (including any lockout). + content: + application/json: + schema: + $ref: '#/components/schemas/RecordSecurityEventResponse' '400': description: Invalid event type content: @@ -1674,16 +1567,22 @@ paths: 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' - /customers/{customerId}/sca/factors/reset: + /sca/factors/reset: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer resetting a factor. required: true schema: @@ -1696,7 +1595,7 @@ paths: flow. Opens the liveness check and returns a `resetId` plus the opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end user completes it with. Poll - `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness + `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`. @@ -1742,16 +1641,22 @@ paths: 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' - /customers/{customerId}/sca/factors/reset/{resetId}: + /sca/factors/reset/{resetId}: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer whose reset status is polled. required: true schema: @@ -1805,10 +1710,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors/reset/{resetId}/complete: + /sca/factors/reset/{resetId}/complete: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer completing the reset. required: true schema: @@ -1826,12 +1731,21 @@ paths: 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/TwoFactorResetCompleteRequest' responses: '204': description: Reset completed; no content is returned. @@ -1865,15 +1779,8 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/external-accounts/{externalAccountId}/trust: + /customers/external-accounts/{externalAccountId}/trust: parameters: - - name: customerId - in: path - description: The unique identifier of the customer who owns the external account. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - name: externalAccountId in: path description: The unique identifier of the external account (beneficiary) being trusted. @@ -1886,7 +1793,7 @@ paths: 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/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. + `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 @@ -1931,15 +1838,8 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm: + /customers/external-accounts/{externalAccountId}/trust/confirm: parameters: - - name: customerId - in: path - description: The unique identifier of the customer who owns the external account. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - name: externalAccountId in: path description: The unique identifier of the external account (beneficiary) being trusted. @@ -2005,15 +1905,68 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/external-accounts/{externalAccountId}/untrust/confirm: + /customers/external-accounts/{externalAccountId}/untrust: parameters: - - name: customerId + - name: externalAccountId in: path - description: The unique identifier of the customer who owns the external account. + description: The unique identifier of the external account (beneficiary) being untrusted. required: true schema: type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 + 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/BeneficiaryTrustStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + 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' + /customers/external-accounts/{externalAccountId}/untrust/confirm: + parameters: - name: externalAccountId in: path description: The unique identifier of the external account (beneficiary) being untrusted. @@ -11676,12 +11629,14 @@ components: | 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 @@ -11962,14 +11917,54 @@ components: description: The customer's enrolled SCA factors. items: $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 + enum: + - TOTP + description: Discriminator selecting the TOTP factor. TOTP enrollment needs no other input at start. + PasskeyFactorEnrollRequest: + 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. + 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 /customers/{customerId}/sca/factors/totp`; the customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with the first code their app produces. + 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. @@ -11980,44 +11975,22 @@ components: 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 - 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: - - secret - - code - properties: - 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' - 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: - - recoveryCodes - properties: - recoveryCodes: - type: array - description: The one-time recovery codes for this TOTP factor. - items: - type: string - example: - - ABCD-EFGH-IJKL - - MNOP-QRST-UVWX 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: - options: - type: object + 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: @@ -12031,13 +12004,49 @@ components: type: string 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: + - 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' 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 + 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`). @@ -12046,14 +12055,60 @@ components: 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 + 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 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/TotpEnrollmentConfirmResponse' + - $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse' + discriminator: + propertyName: type + mapping: + 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). @@ -12153,6 +12208,43 @@ components: 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 + Error423: + 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 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). @@ -12172,6 +12264,35 @@ components: | `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: + - 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 TwoFactorResetStartRequest: type: object description: Selects which enrolled factor to reset via the liveness-gated recovery flow. @@ -12207,22 +12328,105 @@ components: 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' + 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 TwoFactorResetStatus: type: object - description: The status of an in-progress 2FA reset, polled until it reaches the liveness-passed value. + description: The status of an in-progress 2FA reset, polled until it reaches a terminal value. required: - status + - factor + - expiresAt properties: status: type: string - description: The status of the reset, passed through verbatim (Grid does not normalize it). Keep polling while it reports a non-terminal value such as `PENDING`; once it reaches the liveness-passed value (`LIVENESS_PASSED`), call the complete endpoint. Other values may appear, so treat anything other than the liveness-passed sentinel as "not ready yet" rather than failing. + 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: 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 or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. + 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: @@ -19556,20 +19760,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: @@ -19829,60 +20019,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: @@ -20107,33 +20243,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 29a75af46..34d5f7061 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1127,11 +1127,11 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors: + /sca/factors: parameters: - name: customerId - in: path - description: The unique identifier of the customer whose enrolled factors are listed. + in: query + description: The unique identifier of the customer whose factors are listed or enrolled. required: true schema: type: string @@ -1178,86 +1178,20 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors/totp: - parameters: - - name: customerId - in: path - description: The unique identifier of the customer enrolling a TOTP factor. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Start TOTP factor enrollment + summary: Start SCA factor enrollment description: | - Begin enrolling a time-based one-time-password (TOTP) authenticator factor - for the customer. Returns the shared secret and an `otpauth://` provisioning - URI; the customer scans it into an authenticator app and confirms with the - first code via `POST /customers/{customerId}/sca/factors/totp/confirm`. + Begin enrolling an SCA factor for the customer. The request body's `type` + selects the factor — `TOTP` or `PASSKEY`. `SMS_OTP` is not enrollable (it uses + the customer's verified phone). Returns the factor-specific material needed to + finish via `POST /sca/factors/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: startTotpFactorEnrollment - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - responses: - '200': - description: TOTP enrollment started; the shared secret is returned. - content: - application/json: - schema: - $ref: '#/components/schemas/TotpEnrollmentStart' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - 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' - /customers/{customerId}/sca/factors/totp/confirm: - parameters: - - name: customerId - in: path - description: The unique identifier of the customer confirming a TOTP factor. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - post: - summary: Confirm TOTP factor enrollment - description: | - Finalize TOTP factor enrollment by submitting the shared secret from the - start call and the first code the customer's authenticator app produces. - Returns one-time recovery codes shown to the customer only once. + 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`. - - In sandbox, the code is always `123456`. - operationId: confirmTotpFactorEnrollment + operationId: startScaFactorEnrollment tags: - Strong Customer Authentication security: @@ -1267,74 +1201,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TotpEnrollmentConfirmRequest' - responses: - '200': - description: TOTP factor enrolled; recovery codes are returned. - content: - application/json: - schema: - $ref: '#/components/schemas/TotpEnrollmentConfirmResponse' - '400': - description: Invalid or incorrect confirmation code - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - 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' - /customers/{customerId}/sca/factors/passkey: - parameters: - - name: customerId - in: path - description: The unique identifier of the customer enrolling a passkey factor. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - post: - summary: Start passkey factor enrollment - description: | - Begin enrolling a WebAuthn passkey factor for the customer. Returns opaque - WebAuthn registration `options`; pass them to the device's WebAuthn API and - submit the resulting credential via - `POST /customers/{customerId}/sca/factors/passkey/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: startPasskeyFactorEnrollment - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] + $ref: '#/components/schemas/ScaFactorEnrollRequestOneOf' responses: '200': - description: Passkey enrollment started; WebAuthn registration options are returned. + description: Enrollment started; the factor-specific completion material is returned. content: application/json: schema: - $ref: '#/components/schemas/PasskeyEnrollmentStart' + $ref: '#/components/schemas/ScaFactorEnrollStartOneOf' '400': description: Invalid request content: @@ -1354,7 +1228,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: SCA is not required for this customer. + 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: @@ -1365,24 +1239,32 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors/passkey/confirm: + /sca/factors/confirm: parameters: - name: customerId - in: path - description: The unique identifier of the customer confirming a passkey factor. + 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 passkey factor enrollment + summary: Confirm SCA factor enrollment description: | - Finalize passkey factor enrollment by submitting the WebAuthn credential the - device produced for the registration challenge, along with the origin it was - produced against. Returns the enrolled factor. + 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`. - operationId: confirmPasskeyFactorEnrollment + + In sandbox, the TOTP code is always `123456`. + operationId: confirmScaFactorEnrollment tags: - Strong Customer Authentication security: @@ -1392,16 +1274,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PasskeyEnrollmentConfirmRequest' + $ref: '#/components/schemas/ScaFactorConfirmRequestOneOf' responses: '200': - description: Passkey factor enrolled; the enrolled factor is returned. + description: Factor enrolled; the factor-specific result is returned. content: application/json: schema: - $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse' + $ref: '#/components/schemas/ScaFactorConfirmResponseOneOf' '400': - description: Invalid credential or origin + description: Invalid or incorrect confirmation proof content: application/json: schema: @@ -1430,35 +1312,36 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors/passkey/{credentialId}: + /sca/factors/{credentialId}: parameters: - name: customerId - in: path - description: The unique identifier of the customer whose passkey is being deleted. + 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 passkey to delete (from the enrolled factor's `credentialId`). + 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 passkey factor + summary: Delete an enrolled SCA factor description: | - Delete an enrolled WebAuthn passkey factor by its credential id. + 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: deletePasskeyFactor + operationId: deleteScaFactor tags: - Strong Customer Authentication security: - BasicAuth: [] responses: '204': - description: Passkey deleted; no content is returned. + description: Factor deleted; no content is returned. '401': description: Unauthorized content: @@ -1466,7 +1349,7 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Customer or passkey not found + description: Customer or factor not found content: application/json: schema: @@ -1483,10 +1366,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/login/start: + /sca/login/start: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer starting an SCA login. required: true schema: @@ -1501,7 +1384,7 @@ paths: 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 /customers/{customerId}/sca/login/complete`. + `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 @@ -1552,10 +1435,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/login/complete: + /sca/login/complete: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer completing an SCA login. required: true schema: @@ -1614,16 +1497,22 @@ paths: 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' - /customers/{customerId}/sca/record-event: + /sca/record-event: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer the security event is recorded for. required: true schema: @@ -1648,8 +1537,12 @@ paths: schema: $ref: '#/components/schemas/RecordSecurityEventRequest' responses: - '204': - description: Event recorded; no content is returned. + '200': + description: Event recorded; the customer's resulting login-security state is returned (including any lockout). + content: + application/json: + schema: + $ref: '#/components/schemas/RecordSecurityEventResponse' '400': description: Invalid event type content: @@ -1674,16 +1567,22 @@ paths: 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' - /customers/{customerId}/sca/factors/reset: + /sca/factors/reset: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer resetting a factor. required: true schema: @@ -1696,7 +1595,7 @@ paths: flow. Opens the liveness check and returns a `resetId` plus the opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end user completes it with. Poll - `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness + `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`. @@ -1742,16 +1641,22 @@ paths: 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' - /customers/{customerId}/sca/factors/reset/{resetId}: + /sca/factors/reset/{resetId}: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer whose reset status is polled. required: true schema: @@ -1805,10 +1710,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/sca/factors/reset/{resetId}/complete: + /sca/factors/reset/{resetId}/complete: parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer completing the reset. required: true schema: @@ -1826,12 +1731,21 @@ paths: 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/TwoFactorResetCompleteRequest' responses: '204': description: Reset completed; no content is returned. @@ -1865,15 +1779,8 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/external-accounts/{externalAccountId}/trust: + /customers/external-accounts/{externalAccountId}/trust: parameters: - - name: customerId - in: path - description: The unique identifier of the customer who owns the external account. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - name: externalAccountId in: path description: The unique identifier of the external account (beneficiary) being trusted. @@ -1886,7 +1793,7 @@ paths: 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/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. + `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 @@ -1931,15 +1838,8 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm: + /customers/external-accounts/{externalAccountId}/trust/confirm: parameters: - - name: customerId - in: path - description: The unique identifier of the customer who owns the external account. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - name: externalAccountId in: path description: The unique identifier of the external account (beneficiary) being trusted. @@ -2005,15 +1905,68 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/{customerId}/external-accounts/{externalAccountId}/untrust/confirm: + /customers/external-accounts/{externalAccountId}/untrust: parameters: - - name: customerId + - name: externalAccountId in: path - description: The unique identifier of the customer who owns the external account. + description: The unique identifier of the external account (beneficiary) being untrusted. required: true schema: type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 + 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/BeneficiaryTrustStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + 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' + /customers/external-accounts/{externalAccountId}/untrust/confirm: + parameters: - name: externalAccountId in: path description: The unique identifier of the external account (beneficiary) being untrusted. @@ -11676,12 +11629,14 @@ components: | 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 @@ -11962,14 +11917,54 @@ components: description: The customer's enrolled SCA factors. items: $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 + enum: + - TOTP + description: Discriminator selecting the TOTP factor. TOTP enrollment needs no other input at start. + PasskeyFactorEnrollRequest: + 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. + 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 /customers/{customerId}/sca/factors/totp`; the customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with the first code their app produces. + 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. @@ -11980,44 +11975,22 @@ components: 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 - 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: - - secret - - code - properties: - 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' - 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: - - recoveryCodes - properties: - recoveryCodes: - type: array - description: The one-time recovery codes for this TOTP factor. - items: - type: string - example: - - ABCD-EFGH-IJKL - - MNOP-QRST-UVWX 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: - options: - type: object + 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: @@ -12031,13 +12004,49 @@ components: type: string 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: + - 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' 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 + 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`). @@ -12046,14 +12055,60 @@ components: 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 + 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 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/TotpEnrollmentConfirmResponse' + - $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse' + discriminator: + propertyName: type + mapping: + 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). @@ -12153,6 +12208,43 @@ components: 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 + Error423: + 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 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). @@ -12172,6 +12264,35 @@ components: | `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: + - 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 TwoFactorResetStartRequest: type: object description: Selects which enrolled factor to reset via the liveness-gated recovery flow. @@ -12207,22 +12328,105 @@ components: 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' + 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 TwoFactorResetStatus: type: object - description: The status of an in-progress 2FA reset, polled until it reaches the liveness-passed value. + description: The status of an in-progress 2FA reset, polled until it reaches a terminal value. required: - status + - factor + - expiresAt properties: status: type: string - description: The status of the reset, passed through verbatim (Grid does not normalize it). Keep polling while it reports a non-terminal value such as `PENDING`; once it reaches the liveness-passed value (`LIVENESS_PASSED`), call the complete endpoint. Other values may appear, so treat anything other than the liveness-passed sentinel as "not ready yet" rather than failing. + 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: 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 or transaction, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` or `POST /transactions/{transactionId}/authorize`, or inline via the optional `scaAuthorization` field on the originating `execute` / `transfer-out` call. + 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: @@ -19556,20 +19760,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: @@ -19829,60 +20019,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: @@ -20107,33 +20243,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/PasskeyEnrollmentConfirmRequest.yaml b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml index 87e018798..8a514ff86 100644 --- a/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml +++ b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml @@ -3,9 +3,15 @@ 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: >- diff --git a/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml index 78f8431fa..b74b86298 100644 --- a/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml +++ b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml @@ -1,7 +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 index ed2a68d02..952799450 100644 --- a/openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml +++ b/openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml @@ -5,10 +5,16 @@ description: >- 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 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/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/TotpEnrollmentConfirmRequest.yaml b/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml index 9f3e87a21..df6bc1a81 100644 --- a/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml +++ b/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml @@ -4,9 +4,15 @@ description: >- 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: >- diff --git a/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml b/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml index 9aba66bb2..b3d7e9aab 100644 --- a/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml +++ b/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml @@ -4,8 +4,14 @@ description: >- 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. diff --git a/openapi/components/schemas/sca/TotpEnrollmentStart.yaml b/openapi/components/schemas/sca/TotpEnrollmentStart.yaml index cd5e6efc3..26dffef54 100644 --- a/openapi/components/schemas/sca/TotpEnrollmentStart.yaml +++ b/openapi/components/schemas/sca/TotpEnrollmentStart.yaml @@ -1,14 +1,20 @@ type: object description: >- The shared secret a customer's authenticator app needs to enroll a TOTP - factor. Returned by `POST /customers/{customerId}/sca/factors/totp`; the + 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. 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/TwoFactorResetStatus.yaml b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml index ae9e35212..c26598b62 100644 --- a/openapi/components/schemas/sca/TwoFactorResetStatus.yaml +++ b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml @@ -1,17 +1,59 @@ type: object -description: >- - The status of an in-progress 2FA reset, polled until it - reaches the liveness-passed value. +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 status of the reset, passed through verbatim (Grid - does not normalize it). Keep polling while it reports a non-terminal value - such as `PENDING`; once it reaches the liveness-passed value - (`LIVENESS_PASSED`), call the complete endpoint. Other values may appear, - so treat anything other than the liveness-passed - sentinel as "not ready yet" rather than failing. + 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/openapi.yaml b/openapi/openapi.yaml index 3bcab4dff..7254a8e98 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -153,36 +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 - /customers/{customerId}/sca/factors: - $ref: paths/customers/customers_{customerId}_sca_factors.yaml - /customers/{customerId}/sca/factors/totp: - $ref: paths/customers/customers_{customerId}_sca_factors_totp.yaml - /customers/{customerId}/sca/factors/totp/confirm: - $ref: paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml - /customers/{customerId}/sca/factors/passkey: - $ref: paths/customers/customers_{customerId}_sca_factors_passkey.yaml - /customers/{customerId}/sca/factors/passkey/confirm: - $ref: paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml - /customers/{customerId}/sca/factors/passkey/{credentialId}: - $ref: paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml - /customers/{customerId}/sca/login/start: - $ref: paths/customers/customers_{customerId}_sca_login_start.yaml - /customers/{customerId}/sca/login/complete: - $ref: paths/customers/customers_{customerId}_sca_login_complete.yaml - /customers/{customerId}/sca/record-event: - $ref: paths/customers/customers_{customerId}_sca_record-event.yaml - /customers/{customerId}/sca/factors/reset: - $ref: paths/customers/customers_{customerId}_sca_factors_reset.yaml - /customers/{customerId}/sca/factors/reset/{resetId}: - $ref: paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml - /customers/{customerId}/sca/factors/reset/{resetId}/complete: - $ref: paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml - /customers/{customerId}/external-accounts/{externalAccountId}/trust: - $ref: paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml - /customers/{customerId}/external-accounts/{externalAccountId}/trust/confirm: - $ref: paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml - /customers/{customerId}/external-accounts/{externalAccountId}/untrust/confirm: - $ref: paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_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_{customerId}_external-accounts_{externalAccountId}_trust.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml similarity index 85% rename from openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml rename to openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml index e9abc93ec..52853b191 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust.yaml +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml @@ -1,11 +1,4 @@ parameters: - - name: customerId - in: path - description: The unique identifier of the customer who owns the external account. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - name: externalAccountId in: path description: The unique identifier of the external account (beneficiary) being trusted. @@ -18,7 +11,7 @@ post: 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/{customerId}/external-accounts/{externalAccountId}/trust/confirm`. + `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 diff --git a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml similarity index 90% rename from openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml rename to openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml index 0ad98c8dc..d69d2e888 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_trust_confirm.yaml +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml @@ -1,11 +1,4 @@ parameters: - - name: customerId - in: path - description: The unique identifier of the customer who owns the external account. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - name: externalAccountId in: path description: The unique identifier of the external account (beneficiary) being trusted. diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml similarity index 62% rename from openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml rename to openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml index bd33420a9..85e17b1e3 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_totp.yaml +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml @@ -1,32 +1,32 @@ parameters: - - name: customerId + - name: externalAccountId in: path - description: The unique identifier of the customer enrolling a TOTP factor. + description: The unique identifier of the external account (beneficiary) being untrusted. required: true schema: type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 post: - summary: Start TOTP factor enrollment + summary: Start untrusting a beneficiary description: | - Begin enrolling a time-based one-time-password (TOTP) authenticator factor - for the customer. Returns the shared secret and an `otpauth://` provisioning - URI; the customer scans it into an authenticator app and confirms with the - first code via `POST /customers/{customerId}/sca/factors/totp/confirm`. + 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: startTotpFactorEnrollment + operationId: startBeneficiaryUntrust tags: - Strong Customer Authentication security: - BasicAuth: [] responses: '200': - description: TOTP enrollment started; the shared secret is returned. + description: Beneficiary untrust started; the SCA challenge (if any) is returned. content: application/json: schema: - $ref: ../../components/schemas/sca/TotpEnrollmentStart.yaml + $ref: ../../components/schemas/sca/BeneficiaryTrustStart.yaml '400': description: Invalid request content: @@ -40,7 +40,7 @@ post: schema: $ref: ../../components/schemas/errors/Error401.yaml '404': - description: Customer not found + description: Customer or external account not found content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml similarity index 90% rename from openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml rename to openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml index 70892bcfa..ec5d38a64 100644 --- a/openapi/paths/customers/customers_{customerId}_external-accounts_{externalAccountId}_untrust_confirm.yaml +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml @@ -1,11 +1,4 @@ parameters: - - name: customerId - in: path - description: The unique identifier of the customer who owns the external account. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 - name: externalAccountId in: path description: The unique identifier of the external account (beneficiary) being untrusted. diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors.yaml deleted file mode 100644 index fc8983df6..000000000 --- a/openapi/paths/customers/customers_{customerId}_sca_factors.yaml +++ /dev/null @@ -1,50 +0,0 @@ -parameters: - - name: customerId - in: path - description: The unique identifier of the customer whose enrolled factors are listed. - 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 diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml deleted file mode 100644 index bed3961d5..000000000 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey.yaml +++ /dev/null @@ -1,59 +0,0 @@ -parameters: - - name: customerId - in: path - description: The unique identifier of the customer enrolling a passkey factor. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 -post: - summary: Start passkey factor enrollment - description: | - Begin enrolling a WebAuthn passkey factor for the customer. Returns opaque - WebAuthn registration `options`; pass them to the device's WebAuthn API and - submit the resulting credential via - `POST /customers/{customerId}/sca/factors/passkey/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: startPasskeyFactorEnrollment - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - responses: - '200': - description: Passkey enrollment started; WebAuthn registration options are returned. - content: - application/json: - schema: - $ref: ../../components/schemas/sca/PasskeyEnrollmentStart.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. - 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_{customerId}_sca_factors_passkey_confirm.yaml b/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml deleted file mode 100644 index f4917e08b..000000000 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_confirm.yaml +++ /dev/null @@ -1,64 +0,0 @@ -parameters: - - name: customerId - in: path - description: The unique identifier of the customer confirming a passkey factor. - required: true - schema: - type: string - example: Customer:019542f5-b3e7-1d02-0000-000000000001 -post: - summary: Confirm passkey factor enrollment - description: | - Finalize passkey factor enrollment by submitting the WebAuthn credential the - device produced for the registration challenge, along with the origin it was - produced against. 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`. - operationId: confirmPasskeyFactorEnrollment - tags: - - Strong Customer Authentication - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml - responses: - '200': - description: Passkey factor enrolled; the enrolled factor is returned. - content: - application/json: - schema: - $ref: ../../components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml - '400': - description: Invalid credential or origin - 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.yaml b/openapi/paths/sca/sca_factors.yaml new file mode 100644 index 000000000..b4a716ce1 --- /dev/null +++ b/openapi/paths/sca/sca_factors.yaml @@ -0,0 +1,114 @@ +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. The request body's `type` + selects the factor — `TOTP` or `PASSKEY`. `SMS_OTP` is not enrollable (it uses + the customer's verified phone). Returns the factor-specific material needed to + finish via `POST /sca/factors/confirm`. + + 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/customers/customers_{customerId}_sca_factors_totp_confirm.yaml b/openapi/paths/sca/sca_factors_confirm.yaml similarity index 59% rename from openapi/paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml rename to openapi/paths/sca/sca_factors_confirm.yaml index d41d72bc8..370cc219f 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_totp_confirm.yaml +++ b/openapi/paths/sca/sca_factors_confirm.yaml @@ -1,22 +1,28 @@ parameters: - name: customerId - in: path - description: The unique identifier of the customer confirming a TOTP factor. + 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 TOTP factor enrollment + summary: Confirm SCA factor enrollment description: | - Finalize TOTP factor enrollment by submitting the shared secret from the - start call and the first code the customer's authenticator app produces. - Returns one-time recovery codes shown to the customer only once. + 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 code is always `123456`. - operationId: confirmTotpFactorEnrollment + In sandbox, the TOTP code is always `123456`. + operationId: confirmScaFactorEnrollment tags: - Strong Customer Authentication security: @@ -26,16 +32,16 @@ post: content: application/json: schema: - $ref: ../../components/schemas/sca/TotpEnrollmentConfirmRequest.yaml + $ref: ../../components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml responses: '200': - description: TOTP factor enrolled; recovery codes are returned. + description: Factor enrolled; the factor-specific result is returned. content: application/json: schema: - $ref: ../../components/schemas/sca/TotpEnrollmentConfirmResponse.yaml + $ref: ../../components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml '400': - description: Invalid or incorrect confirmation code + description: Invalid or incorrect confirmation proof content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml b/openapi/paths/sca/sca_factors_reset.yaml similarity index 84% rename from openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml rename to openapi/paths/sca/sca_factors_reset.yaml index 8bf22c2cb..d7fa24870 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_reset.yaml +++ b/openapi/paths/sca/sca_factors_reset.yaml @@ -1,6 +1,6 @@ parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer resetting a factor. required: true schema: @@ -13,7 +13,7 @@ post: flow. Opens the liveness check and returns a `resetId` plus the opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end user completes it with. Poll - `GET /customers/{customerId}/sca/factors/reset/{resetId}` until liveness + `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`. @@ -59,6 +59,14 @@ post: 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: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml b/openapi/paths/sca/sca_factors_reset_{resetId}.yaml similarity index 99% rename from openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml rename to openapi/paths/sca/sca_factors_reset_{resetId}.yaml index 9f70e1284..dcf436481 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}.yaml +++ b/openapi/paths/sca/sca_factors_reset_{resetId}.yaml @@ -1,6 +1,6 @@ parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer whose reset status is polled. required: true schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml b/openapi/paths/sca/sca_factors_reset_{resetId}_complete.yaml similarity index 83% rename from openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml rename to openapi/paths/sca/sca_factors_reset_{resetId}_complete.yaml index 9d05c11ca..2d454df13 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_reset_{resetId}_complete.yaml +++ b/openapi/paths/sca/sca_factors_reset_{resetId}_complete.yaml @@ -1,6 +1,6 @@ parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer completing the reset. required: true schema: @@ -18,12 +18,21 @@ post: 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. diff --git a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml b/openapi/paths/sca/sca_factors_{credentialId}.yaml similarity index 69% rename from openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml rename to openapi/paths/sca/sca_factors_{credentialId}.yaml index 20ee7a133..4e651d44b 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_factors_passkey_{credentialId}.yaml +++ b/openapi/paths/sca/sca_factors_{credentialId}.yaml @@ -1,31 +1,32 @@ parameters: - name: customerId - in: path - description: The unique identifier of the customer whose passkey is being deleted. + 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 passkey to delete (from the enrolled factor's `credentialId`). + 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 passkey factor + summary: Delete an enrolled SCA factor description: | - Delete an enrolled WebAuthn passkey factor by its credential id. + 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: deletePasskeyFactor + operationId: deleteScaFactor tags: - Strong Customer Authentication security: - BasicAuth: [] responses: '204': - description: Passkey deleted; no content is returned. + description: Factor deleted; no content is returned. '401': description: Unauthorized content: @@ -33,7 +34,7 @@ delete: schema: $ref: ../../components/schemas/errors/Error401.yaml '404': - description: Customer or passkey not found + description: Customer or factor not found content: application/json: schema: diff --git a/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml b/openapi/paths/sca/sca_login_complete.yaml similarity index 87% rename from openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml rename to openapi/paths/sca/sca_login_complete.yaml index 08d565c84..8f85304f2 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_login_complete.yaml +++ b/openapi/paths/sca/sca_login_complete.yaml @@ -1,6 +1,6 @@ parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer completing an SCA login. required: true schema: @@ -59,6 +59,14 @@ post: 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: diff --git a/openapi/paths/customers/customers_{customerId}_sca_login_start.yaml b/openapi/paths/sca/sca_login_start.yaml similarity index 97% rename from openapi/paths/customers/customers_{customerId}_sca_login_start.yaml rename to openapi/paths/sca/sca_login_start.yaml index 81864e8a6..1329c138c 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_login_start.yaml +++ b/openapi/paths/sca/sca_login_start.yaml @@ -1,6 +1,6 @@ parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer starting an SCA login. required: true schema: @@ -15,7 +15,7 @@ post: 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 /customers/{customerId}/sca/login/complete`. + `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 diff --git a/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml b/openapi/paths/sca/sca_record-event.yaml similarity index 74% rename from openapi/paths/customers/customers_{customerId}_sca_record-event.yaml rename to openapi/paths/sca/sca_record-event.yaml index 473fb5756..a2dbbabd0 100644 --- a/openapi/paths/customers/customers_{customerId}_sca_record-event.yaml +++ b/openapi/paths/sca/sca_record-event.yaml @@ -1,6 +1,6 @@ parameters: - name: customerId - in: path + in: query description: The unique identifier of the customer the security event is recorded for. required: true schema: @@ -25,8 +25,14 @@ post: schema: $ref: ../../components/schemas/sca/RecordSecurityEventRequest.yaml responses: - '204': - description: Event recorded; no content is returned. + '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: @@ -51,6 +57,14 @@ post: 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: From b7f7e5bd4d59e9a88e1412f3cfc07ab96aedb7d6 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Fri, 17 Jul 2026 09:54:37 -0700 Subject: [PATCH 08/10] docs(sca): drop SCA from transfer-out (no scaFactor, no endpoint SCA docs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transfer-out has no associated quote and does not offer Strong Customer Authentication — per-transaction SCA is authorized only on the quote (POST /quotes/{quoteId}/authorize). Remove the vestigial scaFactor field from TransferOutRequest and the SCA paragraph from the transfer-out 201 response description, so the endpoint carries no SCA surface. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VZsLXjVaJZbjjjoswBmhMu --- mintlify/openapi.yaml | 7 ------- openapi.yaml | 7 ------- .../schemas/transfers/TransferOutRequest.yaml | 8 -------- openapi/paths/transfers/transfer_out.yaml | 11 ----------- 4 files changed, 33 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 34d5f7061..b08bbffdf 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -3333,10 +3333,6 @@ paths: '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: @@ -19782,9 +19778,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: diff --git a/openapi.yaml b/openapi.yaml index 34d5f7061..b08bbffdf 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3333,10 +3333,6 @@ paths: '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: @@ -19782,9 +19778,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: 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/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: From fc1212a96486996f366f7afe5838bac0b2485477 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Fri, 17 Jul 2026 11:02:57 -0700 Subject: [PATCH 09/10] fix(sca): make ScaFactorView credentialId/name plain-optional, not nullable These fields are omitted (not sent as null) for TOTP/SMS_OTP factors, so explicit null was the wrong contract. Drop 'null' from the type; the fields stay optional via absence from `required`. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015635zZx4bhUJHNBs5WdB4c --- mintlify/openapi.yaml | 8 ++------ openapi.yaml | 8 ++------ openapi/components/schemas/sca/ScaFactorView.yaml | 8 ++------ 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index b08bbffdf..ea9213df5 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -11893,14 +11893,10 @@ components: $ref: '#/components/schemas/ScaFactor' description: The kind of enrolled factor. credentialId: - type: - - string - - 'null' + 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 - - 'null' + type: string description: An optional human-readable label for this factor. ScaFactorList: type: object diff --git a/openapi.yaml b/openapi.yaml index b08bbffdf..ea9213df5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11893,14 +11893,10 @@ components: $ref: '#/components/schemas/ScaFactor' description: The kind of enrolled factor. credentialId: - type: - - string - - 'null' + 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 - - 'null' + type: string description: An optional human-readable label for this factor. ScaFactorList: type: object diff --git a/openapi/components/schemas/sca/ScaFactorView.yaml b/openapi/components/schemas/sca/ScaFactorView.yaml index b9d7e9862..1a434f37d 100644 --- a/openapi/components/schemas/sca/ScaFactorView.yaml +++ b/openapi/components/schemas/sca/ScaFactorView.yaml @@ -7,15 +7,11 @@ properties: $ref: ./ScaFactor.yaml description: The kind of enrolled factor. credentialId: - type: - - string - - 'null' + 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 - - 'null' + type: string description: An optional human-readable label for this factor. From 8a6e8a16c10fc7a082e321ead51a9f5402151aa7 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Fri, 17 Jul 2026 11:07:17 -0700 Subject: [PATCH 10/10] docs(sca): clarify SMS_OTP is implicit, not enrolled via factor enrollment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand the enrollment description to explain that SMS_OTP is not enrolled through POST /sca/factors — every customer in an SCA-regulated region has a verified phone from customer creation (Contact Verification flows), so SMS is always available as a factor. TOTP/PASSKEY remain the explicit opt-in factors. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015635zZx4bhUJHNBs5WdB4c --- mintlify/openapi.yaml | 15 +++++++++++---- openapi.yaml | 15 +++++++++++---- openapi/paths/sca/sca_factors.yaml | 15 +++++++++++---- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index ea9213df5..40e821c1a 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -1181,10 +1181,17 @@ paths: post: summary: Start SCA factor enrollment description: | - Begin enrolling an SCA factor for the customer. The request body's `type` - selects the factor — `TOTP` or `PASSKEY`. `SMS_OTP` is not enrollable (it uses - the customer's verified phone). Returns the factor-specific material needed to - finish via `POST /sca/factors/confirm`. + 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 diff --git a/openapi.yaml b/openapi.yaml index ea9213df5..40e821c1a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1181,10 +1181,17 @@ paths: post: summary: Start SCA factor enrollment description: | - Begin enrolling an SCA factor for the customer. The request body's `type` - selects the factor — `TOTP` or `PASSKEY`. `SMS_OTP` is not enrollable (it uses - the customer's verified phone). Returns the factor-specific material needed to - finish via `POST /sca/factors/confirm`. + 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 diff --git a/openapi/paths/sca/sca_factors.yaml b/openapi/paths/sca/sca_factors.yaml index b4a716ce1..5d6da61a6 100644 --- a/openapi/paths/sca/sca_factors.yaml +++ b/openapi/paths/sca/sca_factors.yaml @@ -51,10 +51,17 @@ get: post: summary: Start SCA factor enrollment description: | - Begin enrolling an SCA factor for the customer. The request body's `type` - selects the factor — `TOTP` or `PASSKEY`. `SMS_OTP` is not enrollable (it uses - the customer's verified phone). Returns the factor-specific material needed to - finish via `POST /sca/factors/confirm`. + 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