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/docs.json b/mintlify/docs.json
index e7296c730..a075d2b29 100644
--- a/mintlify/docs.json
+++ b/mintlify/docs.json
@@ -58,6 +58,17 @@
"platform-overview/core-concepts/currencies-and-rails",
"platform-overview/configuration"
]
+ },
+ {
+ "group": "Strong Customer Authentication",
+ "pages": [
+ "platform-overview/sca/overview",
+ "platform-overview/sca/per-transaction-authorization",
+ "platform-overview/sca/factor-enrollment",
+ "platform-overview/sca/login-and-sessions",
+ "platform-overview/sca/trusted-beneficiaries",
+ "platform-overview/sca/two-factor-reset"
+ ]
}
]
},
diff --git a/mintlify/global-p2p/sending-receiving-payments/sending-payments.mdx b/mintlify/global-p2p/sending-receiving-payments/sending-payments.mdx
index d8e1b6b9d..3197247e0 100644
--- a/mintlify/global-p2p/sending-receiving-payments/sending-payments.mdx
+++ b/mintlify/global-p2p/sending-receiving-payments/sending-payments.mdx
@@ -27,4 +27,13 @@ import SendUMA from '/snippets/sending/uma.mdx'
+## Strong Customer Authentication (EU customers)
+
+Customers in SCA-regulated regions (in practice the EU: EUR / USDC) must confirm
+payments with Strong Customer Authentication. When it applies, the payment comes
+back `PENDING_AUTHORIZATION` with an `scaChallenge` that you authorize before the
+transfer is released; for every other customer nothing changes. See
+[Per-transaction authorization](/platform-overview/sca/per-transaction-authorization)
+for the full walkthrough.
+
diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml
index 1e57d7e52..40e821c1a 100644
--- a/mintlify/openapi.yaml
+++ b/mintlify/openapi.yaml
@@ -1127,180 +1127,97 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/internal-accounts:
+ /sca/factors:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer whose factors are listed or enrolled.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
get:
- summary: List Customer internal accounts
+ summary: List enrolled SCA factors
description: |
- Retrieve a list of internal accounts with optional filtering parameters. Returns all
- internal accounts that match the specified filters. If no filters are provided, returns all internal accounts
- (paginated).
+ List the Strong Customer Authentication factors the customer has enrolled.
- Internal accounts are created automatically when a customer is created based on the platform configuration.
- operationId: listCustomerInternalAccounts
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: listScaFactors
tags:
- - Internal Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: currency
- in: query
- description: Filter by currency code
- required: false
- schema:
- type: string
- - name: customerId
- in: query
- description: Filter by internal accounts associated with a specific customer
- required: false
- schema:
- type: string
- - name: type
- in: query
- description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts.
- required: false
- schema:
- $ref: '#/components/schemas/InternalAccountType'
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
responses:
'200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/InternalAccountListResponse'
- '400':
- description: Bad request - Invalid parameters
+ description: The customer's enrolled SCA factors.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/ScaFactorList'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /platform/internal-accounts:
- get:
- summary: List platform internal accounts
- description: |
- Retrieve a list of all internal accounts that belong to the platform, as opposed to an individual customer.
-
- These accounts are created automatically when the platform is configured for each supported currency. They can be used for things like distributing bitcoin rewards to customers, or for other platform-wide purposes.
- operationId: listPlatformInternalAccounts
- tags:
- - Internal Accounts
- security:
- - BasicAuth: []
- parameters:
- - name: currency
- in: query
- description: Filter by currency code
- required: false
- schema:
- type: string
- - name: type
- in: query
- description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts.
- required: false
- schema:
- $ref: '#/components/schemas/InternalAccountType'
- responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/PlatformInternalAccountListResponse'
- '400':
- description: Bad request - Invalid parameters
+ '404':
+ description: Customer not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/external-accounts:
- get:
- summary: List Customer external accounts
+ post:
+ summary: Start SCA factor enrollment
description: |
- Retrieve a list of external accounts with optional filtering parameters. Returns all
- external accounts that match the specified filters. If no filters are provided, returns all external accounts
- (paginated).
+ Begin enrolling an SCA factor for the customer. Enrollment covers the
+ explicit, opt-in factors a customer chooses to add — the request body's
+ `type` selects `TOTP` or `PASSKEY`. Returns the factor-specific material
+ needed to finish via `POST /sca/factors/confirm`.
- External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform.
- operationId: listCustomerExternalAccounts
+ `SMS_OTP` is implicit and is not enrolled through this endpoint. Every
+ customer in an SCA-regulated region has a verified phone number from
+ customer creation (via the Contact Verification flows —
+ `POST /customers/{customerId}/verify-phone` and `.../verify-phone/confirm`),
+ so SMS is always available as a factor with no extra setup and appears
+ among the customer's enrolled factors in `GET /sca/factors`.
+
+ A customer may have **only one passkey**. Starting a passkey enrollment when
+ one is already enrolled returns `409` (`PASSKEY_ALREADY_ENROLLED`) — delete it
+ via `DELETE /sca/factors/{credentialId}` first.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startScaFactorEnrollment
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: currency
- in: query
- description: Filter by currency code
- required: false
- schema:
- type: string
- - name: customerId
- in: query
- description: Filter by external accounts associated with a specific customer
- required: false
- schema:
- type: string
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ScaFactorEnrollRequestOneOf'
responses:
'200':
- description: Successful operation
+ description: Enrollment started; the factor-specific completion material is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccountListResponse'
+ $ref: '#/components/schemas/ScaFactorEnrollStartOneOf'
'400':
- description: Bad request - Invalid parameters
+ description: Invalid request
content:
application/json:
schema:
@@ -1311,18 +1228,52 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer (`CONFLICT`), or a passkey enrollment was requested while one is already enrolled (`PASSKEY_ALREADY_ENROLLED`) — only one passkey per customer is supported.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /sca/factors/confirm:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer confirming a factor enrollment.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
post:
- summary: Add a new external account
- description: Register a new external bank account for a customer.
- operationId: createCustomerExternalAccount
+ summary: Confirm SCA factor enrollment
+ description: |
+ Finalize the factor enrollment started by `POST /sca/factors`. The request
+ body is discriminated by `type`: for `TOTP`, submit the shared `secret` from
+ the start call plus the first `code`; for `PASSKEY`, submit the WebAuthn
+ `credential` the device produced plus the `origin` it was produced against.
+ The threaded secret/credential binds the confirmation to its enrollment, so
+ no separate id is needed.
+
+ A TOTP confirmation returns one-time recovery codes (shown once); a passkey
+ confirmation returns the enrolled factor.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the TOTP code is always `123456`.
+ operationId: confirmScaFactorEnrollment
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
requestBody:
@@ -1330,48 +1281,16 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccountCreateRequest'
- examples:
- usBankAccount:
- summary: Create external US bank account
- value:
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- currency: USD
- accountInfo:
- accountType: USD_ACCOUNT
- accountNumber: '12345678901'
- routingNumber: '123456789'
- bankAccountType: CHECKING
- bankName: Chase Bank
- platformAccountId: ext_acc_123456
- beneficiary:
- beneficiaryType: INDIVIDUAL
- fullName: John Doe
- birthDate: '1990-01-15'
- nationality: US
- address:
- line1: 123 Main Street
- city: San Francisco
- state: CA
- postalCode: '94105'
- country: US
- sparkWallet:
- summary: Create external Spark wallet
- value:
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- currency: BTC
- accountInfo:
- accountType: SPARK_WALLET
- address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
+ $ref: '#/components/schemas/ScaFactorConfirmRequestOneOf'
responses:
- '201':
- description: External account created successfully
+ '200':
+ description: Factor enrolled; the factor-specific result is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccount'
+ $ref: '#/components/schemas/ScaFactorConfirmResponseOneOf'
'400':
- description: Bad request
+ description: Invalid or incorrect confirmation proof
content:
application/json:
schema:
@@ -1382,8 +1301,14 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'409':
- description: Conflict - External account already exists
+ description: SCA is not required for this customer.
content:
application/json:
schema:
@@ -1394,29 +1319,36 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/external-accounts/{externalAccountId}:
+ /sca/factors/{credentialId}:
parameters:
- - name: externalAccountId
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer whose factor is being deleted.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ - name: credentialId
in: path
- description: System-generated unique external account identifier
+ description: The credential id of the enrolled factor to delete (from the factor's `credentialId`).
required: true
schema:
type: string
- get:
- summary: Get customer external account by ID
- description: Retrieve a customer external account by its system-generated ID
- operationId: getCustomerExternalAccountById
+ delete:
+ summary: Delete an enrolled SCA factor
+ description: |
+ Delete an enrolled SCA factor by its credential id. Today only `PASSKEY`
+ factors carry a `credentialId` and are deletable this way.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: deleteScaFactor
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ExternalAccount'
+ '204':
+ description: Factor deleted; no content is returned.
'401':
description: Unauthorized
content:
@@ -1424,89 +1356,64 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: External account not found
+ description: Customer or factor not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
+ '409':
+ description: SCA is not required for this customer.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- delete:
- summary: Delete customer external account by ID
- description: Delete a customer external account by its system-generated ID
- operationId: deleteCustomerExternalAccountById
- tags:
- - External Accounts
- security:
- - BasicAuth: []
- responses:
- '204':
- description: External account deleted successfully
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: External account not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /platform/external-accounts:
- get:
- summary: List platform external accounts
+ /sca/login/start:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer starting an SCA login.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ post:
+ summary: Start an SCA login
description: |
- Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer.
+ Begin an SCA login for the customer with the chosen factor, opening the
+ end-user SCA session (an exemption gating read / account access beyond the
+ per-transaction window). Returns factor-specific material: `SMS_OTP`
+ dispatches a code and returns a `challengeId` + `expiresAt`; `TOTP` returns
+ only the factor (the customer reads the code from their app); `PASSKEY`
+ returns WebAuthn `passkeyOptions`. Complete with
+ `POST /sca/login/complete`.
- These accounts are used for platform-wide operations such as receiving funds from external sources or managing platform-level payment destinations.
- operationId: listPlatformExternalAccounts
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startScaLogin
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: currency
- in: query
- description: Filter by currency code
- required: false
- schema:
- type: string
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ScaLoginStartRequest'
responses:
'200':
- description: Successful operation
+ description: SCA login started; factor-specific material is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccountListResponse'
+ $ref: '#/components/schemas/ScaLoginStart'
'400':
- description: Bad request - Invalid parameters
+ description: Invalid or unknown factor
content:
application/json:
schema:
@@ -1517,18 +1424,47 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /sca/login/complete:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer completing an SCA login.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
post:
- summary: Add a new platform external account
- description: Register a new external bank account for the platform.
- operationId: createPlatformExternalAccount
+ summary: Complete an SCA login
+ description: |
+ Finalize an SCA login by submitting the proof for the started factor
+ (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for
+ `PASSKEY`), echoing the `challengeId` for `SMS_OTP`. Returns the
+ reported session status.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS/TOTP code is always `123456`.
+ operationId: completeScaLogin
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
requestBody:
@@ -1536,46 +1472,16 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/PlatformExternalAccountCreateRequest'
- examples:
- usBankAccount:
- summary: Create external US bank account
- value:
- currency: USD
- accountInfo:
- accountType: USD_ACCOUNT
- accountNumber: '12345678901'
- routingNumber: '123456789'
- bankAccountType: CHECKING
- bankName: Chase Bank
- platformAccountId: ext_acc_123456
- beneficiary:
- beneficiaryType: INDIVIDUAL
- fullName: John Doe
- birthDate: '1990-01-15'
- nationality: US
- address:
- line1: 123 Main Street
- city: San Francisco
- state: CA
- postalCode: '94105'
- country: US
- sparkWallet:
- summary: Create external Spark wallet
- value:
- currency: BTC
- accountInfo:
- accountType: SPARK_WALLET
- address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
+ $ref: '#/components/schemas/ScaLoginCompleteRequest'
responses:
- '201':
- description: External account created successfully
+ '200':
+ description: SCA login completed; the session status is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccount'
+ $ref: '#/components/schemas/ScaLoginComplete'
'400':
- description: Bad request
+ description: Invalid or expired proof
content:
application/json:
schema:
@@ -1586,41 +1492,70 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'409':
- description: Conflict - External account already exists
+ description: SCA is not required for this customer.
content:
application/json:
schema:
$ref: '#/components/schemas/Error409'
+ '423':
+ description: The customer's login is locked (or suspended) after too many failed attempts. `details.lockedUntil` says when they may retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error423'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /platform/external-accounts/{externalAccountId}:
+ /sca/record-event:
parameters:
- - name: externalAccountId
- in: path
- description: System-generated unique external account identifier
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer the security event is recorded for.
required: true
schema:
type: string
- get:
- summary: Get platform external account by ID
- description: Retrieve a platform external account by its system-generated ID
- operationId: getPlatformExternalAccountById
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ post:
+ summary: Record a security event
+ description: |
+ Record a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view), to
+ feed adaptive-authentication signals.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: recordSecurityEvent
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RecordSecurityEventRequest'
responses:
'200':
- description: Successful operation
+ description: Event recorded; the customer's resulting login-security state is returned (including any lockout).
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccount'
+ $ref: '#/components/schemas/RecordSecurityEventResponse'
+ '400':
+ description: Invalid event type
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -1628,54 +1563,52 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: External account not found
+ description: Customer not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- delete:
- summary: Delete platform external account by ID
- description: Delete a platform external account by its system-generated ID
- operationId: deletePlatformExternalAccountById
- tags:
- - External Accounts
- security:
- - BasicAuth: []
- responses:
- '204':
- description: External account deleted successfully
- '401':
- description: Unauthorized
+ '409':
+ description: SCA is not required for this customer.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: External account not found
+ $ref: '#/components/schemas/Error409'
+ '423':
+ description: The customer's login is locked (or suspended) after too many failed attempts. `details.lockedUntil` says when they may retry.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error423'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /beneficial-owners:
+ /sca/factors/reset:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer resetting a factor.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
post:
- summary: Create a beneficial owner
+ summary: Start a 2FA reset
description: |
- Add a beneficial owner, director, or company officer to a business customer. The beneficial owner will go through KYC verification automatically.
- operationId: createBeneficialOwner
+ Begin recovering a lost enrolled factor via a liveness-gated, poll-based
+ flow. Opens the liveness check and returns a `resetId` plus the
+ opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end
+ user completes it with. Poll
+ `GET /sca/factors/reset/{resetId}` until liveness
+ passes, then call the complete endpoint.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startTwoFactorReset
tags:
- - KYC/KYB Verifications
+ - Strong Customer Authentication
security:
- BasicAuth: []
requestBody:
@@ -1683,16 +1616,16 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/BeneficialOwnerCreateRequest'
+ $ref: '#/components/schemas/TwoFactorResetStartRequest'
responses:
'201':
- description: Beneficial owner created successfully
+ description: Reset initiated; the reset handle and liveness material are returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/BeneficialOwner'
+ $ref: '#/components/schemas/TwoFactorResetStart'
'400':
- description: Bad request - Invalid parameters
+ description: Invalid or unknown factor
content:
application/json:
schema:
@@ -1709,139 +1642,122 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '429':
+ description: Too many reset attempts. Reset initiation is rate-limited to 5 per 24 hours per customer; retry after the window indicated by `Retry-After`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /sca/factors/reset/{resetId}:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer whose reset status is polled.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ - name: resetId
+ in: path
+ description: The reset handle returned by the start call.
+ required: true
+ schema:
+ type: string
get:
- summary: List beneficial owners
+ summary: Get 2FA reset status
description: |
- Retrieve a list of beneficial owners for a business customer.
- operationId: listBeneficialOwners
+ Poll the status of an in-progress 2FA reset until it reaches the liveness-passed value, after which the reset can be completed.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: getTwoFactorResetStatus
tags:
- - KYC/KYB Verifications
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: customerId
- in: query
- description: The business customer ID
- required: true
- schema:
- type: string
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
responses:
'200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/BeneficialOwnerListResponse'
- '400':
- description: Bad request - Invalid parameters
+ description: The current reset status.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/TwoFactorResetStatus'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /beneficial-owners/{beneficialOwnerId}:
- get:
- summary: Get a beneficial owner
- description: Retrieve details of a specific beneficial owner by ID.
- operationId: getBeneficialOwner
- tags:
- - KYC/KYB Verifications
- security:
- - BasicAuth: []
- parameters:
- - name: beneficialOwnerId
- in: path
- description: Beneficial owner ID
- required: true
- schema:
- type: string
- responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/BeneficialOwner'
- '401':
- description: Unauthorized
+ '404':
+ description: Customer or reset not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Beneficial owner not found
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- patch:
- summary: Update a beneficial owner
- description: Update details of a specific beneficial owner. Only provided fields are updated.
- operationId: updateBeneficialOwner
+ /sca/factors/reset/{resetId}/complete:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer completing the reset.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ - name: resetId
+ in: path
+ description: The reset handle returned by the start call.
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Complete a 2FA reset
+ description: |
+ Complete a 2FA reset once liveness has passed, clearing the lost factor so
+ the customer can re-enroll.
+
+ For an `SMS_OTP` reset, supply the new `mobile` number in the body — completing
+ the reset enrolls it as the customer's number. Other factors need no body.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: completeTwoFactorReset
tags:
- - KYC/KYB Verifications
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: beneficialOwnerId
- in: path
- description: Beneficial owner ID
- required: true
- schema:
- type: string
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/BeneficialOwnerUpdateRequest'
+ $ref: '#/components/schemas/TwoFactorResetCompleteRequest'
responses:
- '200':
- description: Beneficial owner updated successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/BeneficialOwner'
+ '204':
+ description: Reset completed; no content is returned.
'400':
- description: Bad request - Invalid parameters
+ description: Reset not ready (liveness not yet passed)
content:
application/json:
schema:
@@ -1853,40 +1769,54 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Beneficial owner not found
+ description: Customer or reset not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /documents:
+ /customers/external-accounts/{externalAccountId}/trust:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being trusted.
+ required: true
+ schema:
+ type: string
post:
- summary: Upload a document
+ summary: Start trusting a beneficiary
description: |
- Upload a verification document for a customer or beneficial owner. The request must use multipart/form-data with the file in the `file` field and metadata in the remaining fields.
+ Begin trusting (whitelisting) an external account so future sends to it can
+ skip the per-transaction SCA ceremony. Returns the `scaChallenge` to satisfy
+ (when one is issued). Complete with
+ `POST /customers/external-accounts/{externalAccountId}/trust/confirm`.
- Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB.
- operationId: uploadDocument
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startBeneficiaryTrust
tags:
- - Documents
+ - Strong Customer Authentication
security:
- BasicAuth: []
- requestBody:
- $ref: '#/components/requestBodies/DocumentUploadRequestBody'
responses:
- '201':
- description: Document uploaded successfully
+ '200':
+ description: Beneficiary trust started; the SCA challenge (if any) is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/Document'
+ $ref: '#/components/schemas/BeneficiaryTrustStart'
'400':
- description: Bad request - Invalid file type, size, or parameters
+ description: Invalid request
content:
application/json:
schema:
@@ -1898,57 +1828,62 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Document holder not found
+ description: Customer or external account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- get:
- summary: List documents
+ /customers/external-accounts/{externalAccountId}/trust/confirm:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being trusted.
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Confirm trusting a beneficiary
description: |
- Retrieve a list of documents with optional filtering by document holder.
- operationId: listDocuments
+ Finalize trusting a beneficiary (identified by the `externalAccountId` in the
+ path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or
+ `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when
+ one was issued. Returns `trusted: true`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS/TOTP code is always `123456`.
+ operationId: confirmBeneficiaryTrust
tags:
- - Documents
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: documentHolder
- in: query
- description: Filter by document holder ID (Customer or BeneficialOwner)
- required: false
- schema:
- type: string
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest'
responses:
'200':
- description: Successful operation
+ description: Beneficiary trusted.
content:
application/json:
schema:
- $ref: '#/components/schemas/DocumentListResponse'
+ $ref: '#/components/schemas/BeneficiaryTrustConfirm'
'400':
- description: Bad request - Invalid parameters
+ description: Invalid or expired proof
content:
application/json:
schema:
@@ -1959,35 +1894,60 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer or external account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /documents/{documentId}:
- get:
- summary: Get a document by ID
- description: Retrieve details and metadata of a specific document by ID.
- operationId: getDocument
+ /customers/external-accounts/{externalAccountId}/untrust:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being untrusted.
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Start untrusting a beneficiary
+ description: |
+ Begin untrusting (removing the trusted mark from) an external account, so
+ future sends to it are dynamically linked and require the per-transaction SCA
+ ceremony again. Returns the `scaChallenge` to satisfy (when one is issued).
+ Complete with
+ `POST /customers/external-accounts/{externalAccountId}/untrust/confirm`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startBeneficiaryUntrust
tags:
- - Documents
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: documentId
- in: path
- description: Document ID
- required: true
- schema:
- type: string
responses:
'200':
- description: Successful operation
+ description: Beneficiary untrust started; the SCA challenge (if any) is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/Document'
+ $ref: '#/components/schemas/BeneficiaryTrustStart'
+ '400':
+ description: Invalid request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -1995,44 +1955,62 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Document not found
+ description: Customer or external account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- put:
- summary: Replace a document
+ /customers/external-accounts/{externalAccountId}/untrust/confirm:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being untrusted.
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Confirm untrusting a beneficiary
description: |
- Replace an existing document with a new file and/or updated metadata. This is useful when a document was rejected and needs to be re-uploaded. The request must use multipart/form-data.
- operationId: replaceDocument
+ Finalize untrusting a beneficiary (identified by the `externalAccountId` in
+ the path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or
+ `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when
+ one was issued. Returns `trusted: false`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS/TOTP code is always `123456`.
+ operationId: confirmBeneficiaryUntrust
tags:
- - Documents
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: documentId
- in: path
- description: Document ID
- required: true
- schema:
- type: string
requestBody:
- $ref: '#/components/requestBodies/DocumentReplaceRequestBody'
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest'
responses:
'200':
- description: Document replaced successfully
+ description: Beneficiary untrusted.
content:
application/json:
schema:
- $ref: '#/components/schemas/Document'
+ $ref: '#/components/schemas/BeneficiaryTrustConfirm'
'400':
- description: Bad request - Invalid file type, size, or parameters
+ description: Invalid or expired proof
content:
application/json:
schema:
@@ -2044,158 +2022,128 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Document not found
+ description: Customer or external account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- delete:
- summary: Delete a document
+ /customers/internal-accounts:
+ get:
+ summary: List Customer internal accounts
description: |
- Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable.
- operationId: deleteDocument
+ Retrieve a list of internal accounts with optional filtering parameters. Returns all
+ internal accounts that match the specified filters. If no filters are provided, returns all internal accounts
+ (paginated).
+
+ Internal accounts are created automatically when a customer is created based on the platform configuration.
+ operationId: listCustomerInternalAccounts
tags:
- - Documents
+ - Internal Accounts
security:
- BasicAuth: []
parameters:
- - name: documentId
- in: path
- description: Document ID
- required: true
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
+ schema:
+ type: string
+ - name: customerId
+ in: query
+ description: Filter by internal accounts associated with a specific customer
+ required: false
+ schema:
+ type: string
+ - name: type
+ in: query
+ description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts.
+ required: false
+ schema:
+ $ref: '#/components/schemas/InternalAccountType'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
schema:
type: string
responses:
- '204':
- description: Document deleted successfully
- '401':
- description: Unauthorized
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Document not found
+ $ref: '#/components/schemas/InternalAccountListResponse'
+ '400':
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Document cannot be deleted (already submitted for verification)
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/Error401'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /verifications:
- post:
- summary: Submit customer for verification
+ /platform/internal-accounts:
+ get:
+ summary: List platform internal accounts
description: |
- Trigger KYC (individual) or KYB (business) verification for a customer.
- The response indicates whether all required information has been provided.
- If data is missing, the `errors` array describes exactly what needs to be
- supplied before verification can proceed.
-
- Call this endpoint again after resolving errors to re-submit.
-
- ### What to collect for KYB
-
- Before submitting a `BUSINESS` customer, collect the following via
- `POST /customers`, `POST /beneficial-owners`, and `POST /documents`:
-
- **Business identifying information**
- - Entity full legal name
- - Doing Business As (DBA) name, if applicable
- - Physical address — principal place of business
- - Countries of operation
- - Identification number — U.S. taxpayer identification number, or, for a
- foreign business without one, alternative government-issued documentation
- certifying the existence of the business
-
- **Ownership and control structure** — collected for **one control person**
- (an individual with significant responsibility to control, manage, or
- direct the legal entity) **and all beneficial owners** (every individual
- who owns 25% or more, directly or indirectly). For each, provide:
- - Full name
- - Date of birth
- - Address
- - Identification number:
- - U.S. persons — SSN or ITIN
- - Non-U.S. persons — one or more of: ITIN, passport (with country of
- issuance), alien identification card, or another government-issued
- photo ID evidencing nationality or residence
+ Retrieve a list of all internal accounts that belong to the platform, as opposed to an individual customer.
- **Required documents**
- - Company formation and existence documents (certificate of incorporation,
- articles of association, etc.)
- - Proof of ownership and control structure (organization and ownership
- chart, shareholder agreements, operating agreements, register of members,
- or certification of controlling person and beneficial owners)
- - Proof of address dated within the last 3 months (utility bill, bank
- statement, lease agreement, or official correspondence)
- - Tax ID or equivalent identifying-number documents
- - For non-U.S. beneficial owners — passport plus one additional
- government-issued ID
- operationId: createVerification
+ These accounts are created automatically when the platform is configured for each supported currency. They can be used for things like distributing bitcoin rewards to customers, or for other platform-wide purposes.
+ operationId: listPlatformInternalAccounts
tags:
- - KYC/KYB Verifications
+ - Internal Accounts
security:
- BasicAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VerificationRequest'
+ parameters:
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
+ schema:
+ type: string
+ - name: type
+ in: query
+ description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts.
+ required: false
+ schema:
+ $ref: '#/components/schemas/InternalAccountType'
responses:
'200':
- description: |
- Verification status returned. Check `verificationStatus` and `errors` to determine next steps.
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Verification'
- examples:
- missingInfo:
- summary: Verification blocked by missing data
- value:
- id: Verification:019542f5-b3e7-1d02-0000-000000000001
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- verificationStatus: RESOLVE_ERRORS
- errors:
- - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
- type: MISSING_FIELD
- field: customer.address.line1
- reason: Business address line 1 is required
- - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
- type: MISSING_PROOF_OF_ADDRESS_DOCUMENT
- acceptedDocumentTypes:
- - PROOF_OF_ADDRESS
- reason: Proof of address document is required
- - resourceId: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000002
- type: MISSING_FIELD
- field: personalInfo.birthDate
- reason: Date of birth is required for beneficial owners
- createdAt: '2025-10-03T12:00:00Z'
- submitted:
- summary: Verification submitted successfully
- value:
- id: Verification:019542f5-b3e7-1d02-0000-000000000002
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- verificationStatus: IN_PROGRESS
- errors: []
- createdAt: '2025-10-03T12:00:00Z'
+ $ref: '#/components/schemas/PlatformInternalAccountListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -2208,40 +2156,39 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Customer not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /customers/external-accounts:
get:
- summary: List verifications
+ summary: List Customer external accounts
description: |
- Retrieve a list of verifications with optional filtering by customer ID and status.
- operationId: listVerifications
+ Retrieve a list of external accounts with optional filtering parameters. Returns all
+ external accounts that match the specified filters. If no filters are provided, returns all external accounts
+ (paginated).
+
+ External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform.
+ operationId: listCustomerExternalAccounts
tags:
- - KYC/KYB Verifications
+ - External Accounts
security:
- BasicAuth: []
parameters:
- - name: customerId
+ - name: currency
in: query
- description: Filter by customer ID
+ description: Filter by currency code
required: false
schema:
type: string
- - name: verificationStatus
+ - name: customerId
in: query
- description: Filter by verification status
+ description: Filter by external accounts associated with a specific customer
required: false
schema:
- $ref: '#/components/schemas/VerificationStatus'
+ type: string
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -2263,7 +2210,7 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/VerificationListResponse'
+ $ref: '#/components/schemas/ExternalAccountListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -2282,90 +2229,61 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /verifications/{verificationId}:
- get:
- summary: Get a verification
- description: Retrieve details of a specific verification by ID.
- operationId: getVerification
+ post:
+ summary: Add a new external account
+ description: Register a new external bank account for a customer.
+ operationId: createCustomerExternalAccount
tags:
- - KYC/KYB Verifications
+ - External Accounts
security:
- BasicAuth: []
- parameters:
- - name: verificationId
- in: path
- description: Verification ID
- required: true
- schema:
- type: string
- responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Verification'
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Verification not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /transfer-in:
- post:
- summary: Create a transfer-in request
- description: |
- Transfer funds from an external account to an internal account for a specific customer. This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the paymentInstructions on the internal account to deposit funds.
- operationId: createTransferIn
- tags:
- - Same-Currency Transfers
- security:
- - BasicAuth: []
- parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
- schema:
- type: string
- example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/TransferInRequest'
+ $ref: '#/components/schemas/ExternalAccountCreateRequest'
examples:
- transferIn:
- summary: Transfer from external to internal account
+ usBankAccount:
+ summary: Create external US bank account
value:
- source:
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- amount: 12550
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ currency: USD
+ accountInfo:
+ accountType: USD_ACCOUNT
+ accountNumber: '12345678901'
+ routingNumber: '123456789'
+ bankAccountType: CHECKING
+ bankName: Chase Bank
+ platformAccountId: ext_acc_123456
+ beneficiary:
+ beneficiaryType: INDIVIDUAL
+ fullName: John Doe
+ birthDate: '1990-01-15'
+ nationality: US
+ address:
+ line1: 123 Main Street
+ city: San Francisco
+ state: CA
+ postalCode: '94105'
+ country: US
+ sparkWallet:
+ summary: Create external Spark wallet
+ value:
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ currency: BTC
+ accountInfo:
+ accountType: SPARK_WALLET
+ address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
responses:
'201':
- description: Transfer-in request created successfully
+ description: External account created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionOneOf'
+ $ref: '#/components/schemas/ExternalAccount'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request
content:
application/json:
schema:
@@ -2376,72 +2294,41 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Customer or account not found
+ '409':
+ description: Conflict - External account already exists
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transfer-out:
- post:
- summary: Create a transfer-out request
- description: |
- Transfer funds from an internal account to an external account for a specific customer.
- operationId: createTransferOut
+ /customers/external-accounts/{externalAccountId}:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: System-generated unique external account identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get customer external account by ID
+ description: Retrieve a customer external account by its system-generated ID
+ operationId: getCustomerExternalAccountById
tags:
- - Same-Currency Transfers
+ - External Accounts
security:
- BasicAuth: []
- parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
- schema:
- type: string
- example: 550e8400-e29b-41d4-a716-446655440000
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TransferOutRequest'
- examples:
- transferOut:
- summary: Transfer from internal to external account
- value:
- source:
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- destination:
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- paymentRail: ACH
- amount: 12550
- remittanceInformation: '12345'
responses:
- '201':
- description: |
- Transfer-out request created successfully.
-
- For customers outside SCA-regulated regions the transaction proceeds as usual.
-
- **Strong Customer Authentication (EU):** per-transaction SCA is authorized only on the quote resource (`POST /quotes/{quoteId}/authorize`), and `transfer-out` has no associated quote. `transfer-out` is being deprecated, so SCA-gated EU debits are not offered on this endpoint — use the quote + `execute` flow instead.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TransactionOneOf'
- '400':
- description: Bad request - Invalid parameters
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/ExternalAccount'
'401':
description: Unauthorized
content:
@@ -2449,7 +2336,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Customer or account not found
+ description: External account not found
content:
application/json:
schema:
@@ -2460,49 +2347,17 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /receiver/uma/{receiverUmaAddress}:
- get:
- summary: Look up an UMA address for payment
- description: |
- Lookup a receiving UMA address to determine supported currencies and exchange rates.
- This endpoint helps platforms determine what currencies they can send to a given UMA address.
- operationId: lookupUma
+ delete:
+ summary: Delete customer external account by ID
+ description: Delete a customer external account by its system-generated ID
+ operationId: deleteCustomerExternalAccountById
tags:
- - Cross-Currency Transfers
+ - External Accounts
security:
- BasicAuth: []
- parameters:
- - name: receiverUmaAddress
- in: path
- description: UMA address of the intended recipient
- required: true
- schema:
- type: string
- - name: senderUmaAddress
- in: query
- description: UMA address of the sender (optional if customerId is provided)
- required: false
- schema:
- type: string
- - name: customerId
- in: query
- description: System ID of the sender (optional if senderUmaAddress is provided)
- required: false
- schema:
- type: string
responses:
- '200':
- description: Successful lookup
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ReceiverUmaLookupResponse'
- '400':
- description: Bad request - Missing or invalid parameters
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
+ '204':
+ description: External account deleted successfully
'401':
description: Unauthorized
content:
@@ -2510,69 +2365,60 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: UMA address not found. The address is well-formed but no receiver exists at the counterparty VASP (`UMA_NOT_FOUND`).
+ description: External account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '412':
- description: Counterparty doesn't support UMA version
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error412'
- '424':
- description: Counterparty issue
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /receiver/external-account/{accountId}:
+ /platform/external-accounts:
get:
- summary: Look up an external account for payment
+ summary: List platform external accounts
description: |
- Lookup an external account by ID to determine supported currencies and exchange rates.
- This endpoint helps platforms determine what currencies they can send to a given external account, along with the current estimated exchange rates and minimum and maximum amounts that can be sent.
- operationId: lookupExternalAccount
+ Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer.
+
+ These accounts are used for platform-wide operations such as receiving funds from external sources or managing platform-level payment destinations.
+ operationId: listPlatformExternalAccounts
tags:
- - Cross-Currency Transfers
+ - External Accounts
security:
- BasicAuth: []
parameters:
- - name: accountId
- in: path
- description: System-generated ID of the external account
- required: true
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
schema:
type: string
- example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- - name: senderUmaAddress
+ - name: limit
in: query
- description: UMA address of the sender (optional if customerId is provided)
+ description: Maximum number of results to return (default 20, max 100)
required: false
schema:
- type: string
- - name: customerId
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
in: query
- description: System ID of the sender (optional if senderUmaAddress is provided)
+ description: Cursor for pagination (returned from previous request)
required: false
schema:
type: string
responses:
'200':
- description: Successful lookup
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse'
+ $ref: '#/components/schemas/ExternalAccountListResponse'
'400':
- description: Bad request - Missing or invalid parameters
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -2583,56 +2429,110 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: External account not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '412':
- description: Counterparty doesn't support UMA version
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error412'
- '424':
- description: Counterparty issue
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /quotes/{quoteId}:
- get:
- summary: Get quote by ID
- description: |
- Retrieve a quote by its ID. If the quote has been settled, it will include
- the transaction ID. This allows clients to track the full lifecycle of a payment
- from quote creation to settlement.
- operationId: getQuoteById
- tags:
- - Cross-Currency Transfers
- security:
+ post:
+ summary: Add a new platform external account
+ description: Register a new external bank account for the platform.
+ operationId: createPlatformExternalAccount
+ tags:
+ - External Accounts
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PlatformExternalAccountCreateRequest'
+ examples:
+ usBankAccount:
+ summary: Create external US bank account
+ value:
+ currency: USD
+ accountInfo:
+ accountType: USD_ACCOUNT
+ accountNumber: '12345678901'
+ routingNumber: '123456789'
+ bankAccountType: CHECKING
+ bankName: Chase Bank
+ platformAccountId: ext_acc_123456
+ beneficiary:
+ beneficiaryType: INDIVIDUAL
+ fullName: John Doe
+ birthDate: '1990-01-15'
+ nationality: US
+ address:
+ line1: 123 Main Street
+ city: San Francisco
+ state: CA
+ postalCode: '94105'
+ country: US
+ sparkWallet:
+ summary: Create external Spark wallet
+ value:
+ currency: BTC
+ accountInfo:
+ accountType: SPARK_WALLET
+ address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
+ responses:
+ '201':
+ description: External account created successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExternalAccount'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - External account already exists
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /platform/external-accounts/{externalAccountId}:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: System-generated unique external account identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get platform external account by ID
+ description: Retrieve a platform external account by its system-generated ID
+ operationId: getPlatformExternalAccountById
+ tags:
+ - External Accounts
+ security:
- BasicAuth: []
- parameters:
- - name: quoteId
- in: path
- description: ID of the quote to retrieve
- required: true
- schema:
- type: string
responses:
'200':
- description: Quote retrieved successfully
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/ExternalAccount'
'401':
description: Unauthorized
content:
@@ -2640,7 +2540,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Quote not found
+ description: External account not found
content:
application/json:
schema:
@@ -2651,132 +2551,60 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /quotes:
+ delete:
+ summary: Delete platform external account by ID
+ description: Delete a platform external account by its system-generated ID
+ operationId: deletePlatformExternalAccountById
+ tags:
+ - External Accounts
+ security:
+ - BasicAuth: []
+ responses:
+ '204':
+ description: External account deleted successfully
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: External account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /beneficial-owners:
post:
- summary: Create a transfer quote
+ summary: Create a beneficial owner
description: |
- Generate a quote for a cross-currency transfer between any combination of accounts
- and UMA addresses. This endpoint handles currency exchange and provides the necessary
- instructions to execute the transfer.
-
- **Transfer Types Supported:**
- - **Account to Account**: Transfer between internal/external accounts with currency exchange.
- - **Account to UMA**: Transfer from an internal account to an UMA address.
- - **UMA to Account or UMA to UMA**: This transfer type will only be funded by payment instructions, not from an internal account.
-
- **Key Features:**
- - **Flexible Amount Locking**: Always specify whether you want to lock the sending amount or receiving amount
- - **Currency Exchange**: Handles all cross-currency transfers with real-time exchange rates
- - **Payment Instructions**: For UMA or customer ID sources, provides banking details needed for execution
-
- **Important:** If you are transferring funds in the same currency (no exchange required),
- use the `/transfer-in` or `/transfer-out` endpoints instead.
- operationId: createQuote
+ Add a beneficial owner, director, or company officer to a business customer. The beneficial owner will go through KYC verification automatically.
+ operationId: createBeneficialOwner
tags:
- - Cross-Currency Transfers
+ - KYC/KYB Verifications
security:
- BasicAuth: []
- parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
- schema:
- type: string
- example:
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/QuoteRequest'
- examples:
- accountToAccount:
- summary: Account to Account Transfer
- value:
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- lockedCurrencySide: SENDING
- lockedCurrencyAmount: 10000
- description: Transfer between accounts, either internal or external.
- accountToUma:
- summary: Account to UMA Address Transfer
- value:
- lookupId: LookupRequest:019542f5-b3e7-1d02-0000-000000000009
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: UMA_ADDRESS
- umaAddress: $receiver@uma.domain.com
- currency: EUR
- lockedCurrencySide: SENDING
- lockedCurrencyAmount: 1000
- description: 'Payment for invoice #1234'
- realTimeFundingToSparkWallet:
- summary: Real-time funding to Spark Wallet as an on-ramp flow. Immediate execution.
- value:
- source:
- sourceType: REALTIME_FUNDING
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000009
- currency: USD
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456
- lockedCurrencySide: RECEIVING
- lockedCurrencyAmount: 10000
- immediatelyExecute: true
- description: Bitcoin reward payout!
+ $ref: '#/components/schemas/BeneficialOwnerCreateRequest'
responses:
'201':
- description: |
- Transfer quote created successfully. The response includes exchange rates,
- fees, and transfer details. For transfers involving UMA addresses, payment
- instructions are also included for execution through banking systems.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Quote'
- example:
- id: Quote:019542f5-b3e7-1d02-0000-000000000006
- status: PENDING
- createdAt: '2025-10-03T12:00:00Z'
- expiresAt: '2025-10-03T12:05:00Z'
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- sendingCurrency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- receivingCurrency:
- code: EUR
- name: Euro
- symbol: €
- decimals: 2
- totalSendingAmount: 10000
- totalReceivingAmount: 9200
- exchangeRate: 0.92
- feesIncluded: 10
- transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005
- '202':
- description: |
- Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated.
+ description: Beneficial owner created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/BeneficialOwner'
'400':
- description: Bad request - Missing or invalid parameters
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -2787,172 +2615,145 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '412':
- description: Counterparty doesn't support UMA version
+ '404':
+ description: Customer not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error412'
- '424':
- description: Counterparty issue
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error424'
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error501'
- /quotes/{quoteId}/execute:
- post:
- summary: Execute a quote
+ get:
+ summary: List beneficial owners
description: |
- Execute a quote by its ID. This endpoint initiates the transfer between
- the source and destination accounts.
-
- This endpoint can only be used for quotes with a `source` which is either an internal account,
- or has direct pull functionality (e.g. ACH pull with an external account).
-
- When the quote's `source` is an internal account of type `EMBEDDED_WALLET`,
- the request must include a `Grid-Wallet-Signature` header. The header value
- is the full Grid wallet signature built over the `payloadToSign` value from
- the quote's `paymentInstructions[].accountOrWalletInfo` entry with the
- session private key of a verified authentication credential on the source
- Embedded Wallet.
-
- Once executed, the quote cannot be cancelled and the transfer will be processed.
- operationId: executeQuote
+ Retrieve a list of beneficial owners for a business customer.
+ operationId: listBeneficialOwners
tags:
- - Cross-Currency Transfers
+ - KYC/KYB Verifications
security:
- BasicAuth: []
parameters:
- - name: quoteId
- in: path
+ - name: customerId
+ in: query
+ description: The business customer ID
required: true
- description: The unique identifier of the quote to execute
schema:
type: string
- example: Quote:019542f5-b3e7-1d02-0000-000000000001
- - name: Idempotency-Key
- in: header
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
schema:
- type: string
- example:
- - name: Grid-Wallet-Signature
- in: header
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
required: false
- description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
schema:
type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- requestBody:
- required: false
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ExecuteQuoteRequest'
responses:
'200':
- description: |
- Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers):
-
- - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`).
-
- - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`.
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/BeneficialOwnerListResponse'
'400':
- description: Bad request - Invalid quote ID or quote cannot be confirmed
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Quote not found
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Quote already confirmed, expired, or in invalid state
+ $ref: '#/components/schemas/Error500'
+ /beneficial-owners/{beneficialOwnerId}:
+ get:
+ summary: Get a beneficial owner
+ description: Retrieve details of a specific beneficial owner by ID.
+ operationId: getBeneficialOwner
+ tags:
+ - KYC/KYB Verifications
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: beneficialOwnerId
+ in: path
+ description: Beneficial owner ID
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/BeneficialOwner'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Beneficial owner not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /quotes/{quoteId}/authorize:
- parameters:
- - name: quoteId
- in: path
- description: The unique identifier of the quote whose SCA challenge is being authorized.
- required: true
- schema:
- type: string
- example: Quote:019542f5-b3e7-1d02-0000-000000000006
- post:
- summary: Authorize a quote's SCA challenge
- description: |
- Satisfy the Strong Customer Authentication challenge carried by a quote in
- `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof.
-
- This is used for realtime-funding quotes: the quote is returned with an
- `scaChallenge` and **without** `paymentInstructions`; once authorized, the
- quote advances and its `paymentInstructions` are populated so the customer
- can fund the transfer.
-
- As with all SCA, a quote may require more than one authorization: after
- authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the
- next `scaChallenge` — authorize that too, repeating until it advances (see
- `ScaChallenge`).
-
- This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
-
- In sandbox, the SMS code is always `123456`.
- operationId: authorizeQuote
+ patch:
+ summary: Update a beneficial owner
+ description: Update details of a specific beneficial owner. Only provided fields are updated.
+ operationId: updateBeneficialOwner
tags:
- - Strong Customer Authentication
+ - KYC/KYB Verifications
security:
- BasicAuth: []
+ parameters:
+ - name: beneficialOwnerId
+ in: path
+ description: Beneficial owner ID
+ required: true
+ schema:
+ type: string
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/ScaAuthorization'
+ $ref: '#/components/schemas/BeneficialOwnerUpdateRequest'
responses:
'200':
- description: Challenge authorized; the updated quote is returned.
+ description: Beneficial owner updated successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/BeneficialOwner'
'400':
- description: Invalid or expired authorization proof
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -2964,169 +2765,78 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Quote not found
+ description: Beneficial owner not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '409':
- description: SCA is not required for this customer, or the quote has no pending challenge to authorize.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error409'
- '429':
- description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /quotes/{quoteId}/authorize/resend:
- parameters:
- - name: quoteId
- in: path
- description: The unique identifier of the quote whose SCA challenge code should be re-sent.
- required: true
- schema:
- type: string
- example: Quote:019542f5-b3e7-1d02-0000-000000000006
+ /documents:
post:
- summary: Resend a quote's SCA challenge code
+ summary: Upload a document
description: |
- Re-send the one-time code for a realtime-funding quote in
- `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The
- existing challenge is reused — no new challenge is issued, and its
- `scaChallenge.expiresAt` is **not** extended; once the challenge is past
- `expiresAt` it can no longer be authorized.
-
- Only meaningful for customers in a region where SCA is required (e.g. EU);
- a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409.
+ Upload a verification document for a customer or beneficial owner. The request must use multipart/form-data with the file in the `file` field and metadata in the remaining fields.
- In sandbox, the code is always `123456`.
- operationId: resendQuoteScaCode
+ Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB.
+ operationId: uploadDocument
tags:
- - Strong Customer Authentication
+ - Documents
security:
- BasicAuth: []
+ requestBody:
+ $ref: '#/components/requestBodies/DocumentUploadRequestBody'
responses:
- '204':
- description: Code re-sent.
- '401':
- description: Unauthorized
+ '201':
+ description: Document uploaded successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Quote not found
+ $ref: '#/components/schemas/Document'
+ '400':
+ description: Bad request - Invalid file type, size, or parameters
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey).
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- '429':
- description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Document holder not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error429'
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions:
get:
- summary: List transactions
+ summary: List documents
description: |
- Retrieve a paginated list of transactions with optional filtering.
- The transactions can be filtered by customer ID, platform customer ID, UMA address,
- date range, status, and transaction type.
-
- Card transactions are included and identified by `type: CARD`. In Sandbox this is how
- you discover a `CardTransaction` id after simulating an authorization — list the
- transactions, take the card transaction's `id`, and pass it as the `cardTransactionId`
- to the clearing and return simulate endpoints.
- operationId: listTransactions
- tags:
- - Transactions
+ Retrieve a list of documents with optional filtering by document holder.
+ operationId: listDocuments
+ tags:
+ - Documents
security:
- BasicAuth: []
parameters:
- - name: customerId
- in: query
- description: Filter by system customer ID. To filter to transactions made on behalf of the platform, specify the platform ID as the customer ID.
- required: false
- schema:
- type: string
- - name: platformCustomerId
- in: query
- description: Filter by platform-specific customer ID
- required: false
- schema:
- type: string
- - name: accountIdentifier
- in: query
- description: Filter by account identifier (matches either sender or receiver)
- required: false
- schema:
- type: string
- - name: senderAccountIdentifier
- in: query
- description: Filter by sender account identifier
- required: false
- schema:
- type: string
- - name: receiverAccountIdentifier
- in: query
- description: Filter by receiver account identifier
- required: false
- schema:
- type: string
- - name: status
- in: query
- description: Filter by transaction status
- required: false
- schema:
- $ref: '#/components/schemas/TransactionStatus'
- - name: type
- in: query
- description: Filter by transaction type
- required: false
- schema:
- $ref: '#/components/schemas/TransactionType'
- - name: reference
- in: query
- description: Filter by reference
- required: false
- schema:
- type: string
- - name: startDate
- in: query
- description: Filter by start date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: endDate
+ - name: documentHolder
in: query
- description: Filter by end date (inclusive) in ISO 8601 format
+ description: Filter by document holder ID (Customer or BeneficialOwner)
required: false
schema:
type: string
- format: date-time
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -3142,23 +2852,13 @@ paths:
required: false
schema:
type: string
- - name: sortOrder
- in: query
- description: Order to sort results in
- required: false
- schema:
- type: string
- enum:
- - asc
- - desc
- default: desc
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionListResponse'
+ $ref: '#/components/schemas/DocumentListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -3177,29 +2877,29 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions/{transactionId}:
- parameters:
- - name: transactionId
- in: path
- description: Unique identifier of the transaction
- required: true
- schema:
- type: string
+ /documents/{documentId}:
get:
- summary: Get transaction by ID
- description: Retrieve detailed information about a specific transaction.
- operationId: getTransactionById
+ summary: Get a document by ID
+ description: Retrieve details and metadata of a specific document by ID.
+ operationId: getDocument
tags:
- - Transactions
+ - Documents
security:
- BasicAuth: []
+ parameters:
+ - name: documentId
+ in: path
+ description: Document ID
+ required: true
+ schema:
+ type: string
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionOneOf'
+ $ref: '#/components/schemas/Document'
'401':
description: Unauthorized
content:
@@ -3207,7 +2907,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Transaction not found
+ description: Document not found
content:
application/json:
schema:
@@ -3218,38 +2918,33 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions/{transactionId}/confirm:
- post:
- summary: Confirm receipt delivery
+ put:
+ summary: Replace a document
description: |
- Confirm that the platform delivered the transaction receipt to its customer. This confirmation is only necessary when the platform is contractually required to send a receipt. If `receiptDeliveryConfirmedAt` is omitted, the confirmation time is set to the current server time. Calling this endpoint again for the same transaction updates the stored confirmation time.
- operationId: confirmReceiptDelivery
+ Replace an existing document with a new file and/or updated metadata. This is useful when a document was rejected and needs to be re-uploaded. The request must use multipart/form-data.
+ operationId: replaceDocument
tags:
- - Transactions
+ - Documents
security:
- BasicAuth: []
parameters:
- - name: transactionId
+ - name: documentId
in: path
- description: Unique identifier of the transaction to confirm receipt delivery for
+ description: Document ID
required: true
schema:
type: string
requestBody:
- required: false
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ConfirmReceiptDeliveryRequest'
+ $ref: '#/components/requestBodies/DocumentReplaceRequestBody'
responses:
'200':
- description: Receipt delivery confirmation recorded successfully
+ description: Document replaced successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionOneOf'
+ $ref: '#/components/schemas/Document'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request - Invalid file type, size, or parameters
content:
application/json:
schema:
@@ -3261,7 +2956,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Transaction not found
+ description: Document not found
content:
application/json:
schema:
@@ -3272,43 +2967,25 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions/{transactionId}/approve:
- post:
- summary: Approve a pending incoming payment
+ delete:
+ summary: Delete a document
description: |
- Approve a pending incoming payment that was previously acknowledged with a 202 response.
- This endpoint allows platforms to asynchronously approve payments after async processing.
- operationId: approvePendingPayment
+ Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable.
+ operationId: deleteDocument
tags:
- - Transactions
+ - Documents
security:
- BasicAuth: []
parameters:
- - name: transactionId
+ - name: documentId
in: path
- description: Unique identifier of the transaction to approve
+ description: Document ID
required: true
schema:
type: string
- requestBody:
- required: false
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ApprovePaymentRequest'
responses:
- '200':
- description: Payment approved successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/IncomingTransaction'
- '400':
- description: Bad request - Invalid parameters or payment cannot be approved
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
+ '204':
+ description: Document deleted successfully
'401':
description: Unauthorized
content:
@@ -3316,13 +2993,13 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Transaction not found
+ description: Document not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
'409':
- description: Conflict - Payment is not in a pending state or has already been processed or timed out.
+ description: Conflict - Document cannot be deleted (already submitted for verification)
content:
application/json:
schema:
@@ -3333,99 +3010,172 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions/{transactionId}/reject:
+ /verifications:
post:
- summary: Reject a pending incoming payment
+ summary: Submit customer for verification
description: |
- Reject a pending incoming payment that was previously acknowledged with a 202 response.
- This endpoint allows platforms to asynchronously reject payments after additional processing.
- operationId: rejectPendingPayment
+ Trigger KYC (individual) or KYB (business) verification for a customer.
+ The response indicates whether all required information has been provided.
+ If data is missing, the `errors` array describes exactly what needs to be
+ supplied before verification can proceed.
+
+ Call this endpoint again after resolving errors to re-submit.
+
+ ### What to collect for KYB
+
+ Before submitting a `BUSINESS` customer, collect the following via
+ `POST /customers`, `POST /beneficial-owners`, and `POST /documents`:
+
+ **Business identifying information**
+ - Entity full legal name
+ - Doing Business As (DBA) name, if applicable
+ - Physical address — principal place of business
+ - Countries of operation
+ - Identification number — U.S. taxpayer identification number, or, for a
+ foreign business without one, alternative government-issued documentation
+ certifying the existence of the business
+
+ **Ownership and control structure** — collected for **one control person**
+ (an individual with significant responsibility to control, manage, or
+ direct the legal entity) **and all beneficial owners** (every individual
+ who owns 25% or more, directly or indirectly). For each, provide:
+ - Full name
+ - Date of birth
+ - Address
+ - Identification number:
+ - U.S. persons — SSN or ITIN
+ - Non-U.S. persons — one or more of: ITIN, passport (with country of
+ issuance), alien identification card, or another government-issued
+ photo ID evidencing nationality or residence
+
+ **Required documents**
+ - Company formation and existence documents (certificate of incorporation,
+ articles of association, etc.)
+ - Proof of ownership and control structure (organization and ownership
+ chart, shareholder agreements, operating agreements, register of members,
+ or certification of controlling person and beneficial owners)
+ - Proof of address dated within the last 3 months (utility bill, bank
+ statement, lease agreement, or official correspondence)
+ - Tax ID or equivalent identifying-number documents
+ - For non-U.S. beneficial owners — passport plus one additional
+ government-issued ID
+ operationId: createVerification
tags:
- - Transactions
+ - KYC/KYB Verifications
security:
- BasicAuth: []
- parameters:
- - name: transactionId
- in: path
- description: Unique identifier of the transaction to reject
- required: true
- schema:
- type: string
requestBody:
- required: false
+ required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/RejectPaymentRequest'
+ $ref: '#/components/schemas/VerificationRequest'
responses:
'200':
- description: Payment rejected successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/IncomingTransaction'
- '400':
- description: Bad request - Invalid parameters or payment cannot be rejected
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
+ description: |
+ Verification status returned. Check `verificationStatus` and `errors` to determine next steps.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Transaction not found
+ $ref: '#/components/schemas/Verification'
+ examples:
+ missingInfo:
+ summary: Verification blocked by missing data
+ value:
+ id: Verification:019542f5-b3e7-1d02-0000-000000000001
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ verificationStatus: RESOLVE_ERRORS
+ errors:
+ - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ type: MISSING_FIELD
+ field: customer.address.line1
+ reason: Business address line 1 is required
+ - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ type: MISSING_PROOF_OF_ADDRESS_DOCUMENT
+ acceptedDocumentTypes:
+ - PROOF_OF_ADDRESS
+ reason: Proof of address document is required
+ - resourceId: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000002
+ type: MISSING_FIELD
+ field: personalInfo.birthDate
+ reason: Date of birth is required for beneficial owners
+ createdAt: '2025-10-03T12:00:00Z'
+ submitted:
+ summary: Verification submitted successfully
+ value:
+ id: Verification:019542f5-b3e7-1d02-0000-000000000002
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ verificationStatus: IN_PROGRESS
+ errors: []
+ createdAt: '2025-10-03T12:00:00Z'
+ '400':
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Payment is not in a pending state or has already been processed or timed out.
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /crypto/estimate-withdrawal-fee:
- post:
- summary: Estimate crypto withdrawal fee
+ get:
+ summary: List verifications
description: |
- Estimate the network and application fees for a cryptocurrency withdrawal from a crypto internal account to an external blockchain address. Use this to show fee information to customers before they initiate a withdrawal.
- operationId: estimateCryptoWithdrawalFee
+ Retrieve a list of verifications with optional filtering by customer ID and status.
+ operationId: listVerifications
tags:
- - Cross-Currency Transfers
+ - KYC/KYB Verifications
security:
- BasicAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeRequest'
- examples:
- estimateUSDCWithdrawal:
- summary: Estimate fee for USDC withdrawal on Solana
- value:
- internalAccountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- currency: USDC
- cryptoNetwork: SOLANA
- amount: 1000000
- destinationAddress: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU
+ parameters:
+ - name: customerId
+ in: query
+ description: Filter by customer ID
+ required: false
+ schema:
+ type: string
+ - name: verificationStatus
+ in: query
+ description: Filter by verification status
+ required: false
+ schema:
+ $ref: '#/components/schemas/VerificationStatus'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Fee estimation returned successfully
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeResponse'
+ $ref: '#/components/schemas/VerificationListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -3438,216 +3188,164 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Internal account not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/webhooks/test:
- post:
- summary: Send a test webhook
- description: Send a test webhook to the configured endpoint
- operationId: sendTestWebhook
+ /verifications/{verificationId}:
+ get:
+ summary: Get a verification
+ description: Retrieve details of a specific verification by ID.
+ operationId: getVerification
tags:
- - Sandbox
+ - KYC/KYB Verifications
security:
- BasicAuth: []
+ parameters:
+ - name: verificationId
+ in: path
+ description: Verification ID
+ required: true
+ schema:
+ type: string
responses:
'200':
- description: Webhook delivered successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TestWebhookResponse'
- '400':
- description: Bad request - Webhook delivery failed
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/Verification'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Verification not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/bulk/csv:
+ /transfer-in:
post:
- summary: Upload customers via CSV file
+ summary: Create a transfer-in request
description: |
- Upload a CSV file containing customer information for bulk creation. The CSV file should follow
- a specific format with required and optional columns based on customer type.
-
- ### CSV Format
- The CSV file should have the following columns:
-
- Required columns for all customers:
- - umaAddress: The customer's UMA address (e.g., $john.doe@uma.domain.com)
- - platformCustomerId: Your platform's unique identifier for the customer
- - customerType: Either "INDIVIDUAL" or "BUSINESS"
-
- Required columns for individual customers:
- - fullName: Individual's full name
- - birthDate: Date of birth in YYYY-MM-DD format
- - addressLine1: Street address line 1
- - city: City
- - state: State/Province/Region
- - postalCode: Postal/ZIP code
- - country: Country code (ISO 3166-1 alpha-2)
-
- Required columns for business customers:
- - businessLegalName: Legal name of the business
- - addressLine1: Street address line 1
- - city: City
- - state: State/Province/Region
- - postalCode: Postal/ZIP code
- - country: Country code (ISO 3166-1 alpha-2)
-
- Optional columns for all customers:
- - addressLine2: Street address line 2
- - platformAccountId: Your platform's identifier for the bank account
- - description: Optional description for the customer
-
- Optional columns for individual customers:
- - email: Customer's email address
-
- Optional columns for business customers:
- - businessRegistrationNumber: Business registration number
- - businessTaxId: Tax identification number
-
- ### Example CSV
- ```csv
- umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,platformAccountId,businessLegalName
- john.doe@uma.domain.com,customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US
- acme@uma.domain.com,biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US
- ```
-
- The upload process is asynchronous and will return a job ID that can be used to track progress.
- You can monitor the job status using the `/customers/bulk/jobs/{jobId}` endpoint.
- operationId: uploadCustomersCsv
+ Transfer funds from an external account to an internal account for a specific customer. This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the paymentInstructions on the internal account to deposit funds.
+ operationId: createTransferIn
tags:
- - Customers
+ - Same-Currency Transfers
security:
- BasicAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
- multipart/form-data:
+ application/json:
schema:
- type: object
- required:
- - file
- properties:
- file:
- type: string
- format: binary
- description: CSV file containing customer information
+ $ref: '#/components/schemas/TransferInRequest'
+ examples:
+ transferIn:
+ summary: Transfer from external to internal account
+ value:
+ source:
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ amount: 12550
responses:
- '202':
- description: CSV upload accepted for processing
+ '201':
+ description: Transfer-in request created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/BulkCustomerImportJobAccepted'
+ $ref: '#/components/schemas/TransactionOneOf'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer or account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/bulk/jobs/{jobId}:
- get:
- summary: Get bulk import job status
+ /transfer-out:
+ post:
+ summary: Create a transfer-out request
description: |
- Retrieve the current status and results of a bulk customer import job. This endpoint can be used
- to track the progress of both CSV uploads.
-
- The response includes:
- - Overall job status
- - Progress statistics
- - Detailed error information for failed entries
- - Completion timestamp when finished
- operationId: getBulkCustomerImportJob
+ Transfer funds from an internal account to an external account for a specific customer.
+ operationId: createTransferOut
tags:
- - Customers
+ - Same-Currency Transfers
security:
- BasicAuth: []
parameters:
- - name: jobId
- in: path
- description: ID of the bulk import job to retrieve
- required: true
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
schema:
type: string
- responses:
- '200':
- description: Job status retrieved successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/BulkCustomerImportJob'
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Job not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /invitations:
- post:
- summary: Create an UMA invitation
- description: |
- Create an UMA invitation from a given platform customer.
- operationId: createInvitation
- tags:
- - Invitations
- security:
- - BasicAuth: []
+ example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/UmaInvitationCreateRequest'
+ $ref: '#/components/schemas/TransferOutRequest'
+ examples:
+ transferOut:
+ summary: Transfer from internal to external account
+ value:
+ source:
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ destination:
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ paymentRail: ACH
+ amount: 12550
+ remittanceInformation: '12345'
responses:
'201':
- description: Invitation created successfully
+ description: |
+ Transfer-out request created successfully.
content:
application/json:
schema:
- $ref: '#/components/schemas/UmaInvitation'
+ $ref: '#/components/schemas/TransactionOneOf'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -3658,36 +3356,61 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer or account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /invitations/{invitationCode}:
+ /receiver/uma/{receiverUmaAddress}:
get:
- summary: Get an UMA invitation by code
+ summary: Look up an UMA address for payment
description: |
- Retrieve details about an UMA invitation by its invitation code.
- operationId: getInvitation
+ Lookup a receiving UMA address to determine supported currencies and exchange rates.
+ This endpoint helps platforms determine what currencies they can send to a given UMA address.
+ operationId: lookupUma
tags:
- - Invitations
+ - Cross-Currency Transfers
security:
- BasicAuth: []
parameters:
- - name: invitationCode
+ - name: receiverUmaAddress
in: path
- description: The code of the invitation to get
+ description: UMA address of the intended recipient
required: true
schema:
type: string
+ - name: senderUmaAddress
+ in: query
+ description: UMA address of the sender (optional if customerId is provided)
+ required: false
+ schema:
+ type: string
+ - name: customerId
+ in: query
+ description: System ID of the sender (optional if senderUmaAddress is provided)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Invitation retrieved successfully
+ description: Successful lookup
content:
application/json:
schema:
- $ref: '#/components/schemas/UmaInvitation'
+ $ref: '#/components/schemas/ReceiverUmaLookupResponse'
+ '400':
+ description: Bad request - Missing or invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -3695,56 +3418,69 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Invitation not found
+ description: UMA address not found. The address is well-formed but no receiver exists at the counterparty VASP (`UMA_NOT_FOUND`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '412':
+ description: Counterparty doesn't support UMA version
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error412'
+ '424':
+ description: Counterparty issue
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /invitations/{invitationCode}/claim:
- post:
- summary: Claim an UMA invitation
+ /receiver/external-account/{accountId}:
+ get:
+ summary: Look up an external account for payment
description: |
- Claim an UMA invitation by associating it with an invitee UMA address.
-
- When an invitation is successfully claimed:
- 1. The invitation status changes from PENDING to CLAIMED
- 2. The invitee UMA address is associated with the invitation
- 3. An INVITATION_CLAIMED webhook is triggered to notify the platform that created the invitation
-
- This endpoint allows customers to accept invitations sent to them by other UMA customers.
- operationId: claimInvitation
+ Lookup an external account by ID to determine supported currencies and exchange rates.
+ This endpoint helps platforms determine what currencies they can send to a given external account, along with the current estimated exchange rates and minimum and maximum amounts that can be sent.
+ operationId: lookupExternalAccount
tags:
- - Invitations
+ - Cross-Currency Transfers
security:
- BasicAuth: []
parameters:
- - name: invitationCode
+ - name: accountId
in: path
- description: The code of the invitation to claim
+ description: System-generated ID of the external account
required: true
schema:
type: string
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UmaInvitationClaimRequest'
+ example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ - name: senderUmaAddress
+ in: query
+ description: UMA address of the sender (optional if customerId is provided)
+ required: false
+ schema:
+ type: string
+ - name: customerId
+ in: query
+ description: System ID of the sender (optional if senderUmaAddress is provided)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Invitation claimed successfully
+ description: Successful lookup
content:
application/json:
schema:
- $ref: '#/components/schemas/UmaInvitation'
+ $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse'
'400':
- description: Bad request
+ description: Bad request - Missing or invalid parameters
content:
application/json:
schema:
@@ -3756,69 +3492,63 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Invitation not found
+ description: External account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '412':
+ description: Counterparty doesn't support UMA version
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error412'
+ '424':
+ description: Counterparty issue
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /invitations/{invitationCode}/cancel:
- post:
- summary: Cancel an UMA invitation
+ /quotes/{quoteId}:
+ get:
+ summary: Get quote by ID
description: |
- Cancel a pending UMA invitation. Only the inviter or platform can cancel an invitation.
-
- When an invitation is cancelled:
- 1. The invitation status changes from PENDING to CANCELLED
- 2. The invitation can no longer be claimed
- 3. The invitation URL will show as cancelled when accessed
-
- Only pending invitations can be cancelled. Attempting to cancel an invitation
- that is already claimed, expired, or cancelled will result in an error.
- operationId: cancelInvitation
+ Retrieve a quote by its ID. If the quote has been settled, it will include
+ the transaction ID. This allows clients to track the full lifecycle of a payment
+ from quote creation to settlement.
+ operationId: getQuoteById
tags:
- - Invitations
+ - Cross-Currency Transfers
security:
- BasicAuth: []
parameters:
- - name: invitationCode
+ - name: quoteId
in: path
- description: The code of the invitation to cancel
+ description: ID of the quote to retrieve
required: true
schema:
type: string
responses:
'200':
- description: Invitation cancelled successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UmaInvitation'
- '400':
- description: Bad request - Invitation cannot be cancelled (already claimed, expired, or cancelled)
+ description: Quote retrieved successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/Quote'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Only the platform which created the invitation can cancel it
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Invitation not found
+ description: Quote not found
content:
application/json:
schema:
@@ -3829,32 +3559,132 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/send:
+ /quotes:
post:
- summary: Simulate sending funds
+ summary: Create a transfer quote
description: |
- Simulate sending funds to the bank account as instructed in the quote.
- This endpoint is only for the sandbox environment and will fail for production platforms/keys.
- operationId: sandboxSend
- tags:
- - Sandbox
- security:
- - BasicAuth: []
+ Generate a quote for a cross-currency transfer between any combination of accounts
+ and UMA addresses. This endpoint handles currency exchange and provides the necessary
+ instructions to execute the transfer.
+
+ **Transfer Types Supported:**
+ - **Account to Account**: Transfer between internal/external accounts with currency exchange.
+ - **Account to UMA**: Transfer from an internal account to an UMA address.
+ - **UMA to Account or UMA to UMA**: This transfer type will only be funded by payment instructions, not from an internal account.
+
+ **Key Features:**
+ - **Flexible Amount Locking**: Always specify whether you want to lock the sending amount or receiving amount
+ - **Currency Exchange**: Handles all cross-currency transfers with real-time exchange rates
+ - **Payment Instructions**: For UMA or customer ID sources, provides banking details needed for execution
+
+ **Important:** If you are transferring funds in the same currency (no exchange required),
+ use the `/transfer-in` or `/transfer-out` endpoints instead.
+ operationId: createQuote
+ tags:
+ - Cross-Currency Transfers
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example:
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/SandboxSendRequest'
+ $ref: '#/components/schemas/QuoteRequest'
+ examples:
+ accountToAccount:
+ summary: Account to Account Transfer
+ value:
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ lockedCurrencySide: SENDING
+ lockedCurrencyAmount: 10000
+ description: Transfer between accounts, either internal or external.
+ accountToUma:
+ summary: Account to UMA Address Transfer
+ value:
+ lookupId: LookupRequest:019542f5-b3e7-1d02-0000-000000000009
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: UMA_ADDRESS
+ umaAddress: $receiver@uma.domain.com
+ currency: EUR
+ lockedCurrencySide: SENDING
+ lockedCurrencyAmount: 1000
+ description: 'Payment for invoice #1234'
+ realTimeFundingToSparkWallet:
+ summary: Real-time funding to Spark Wallet as an on-ramp flow. Immediate execution.
+ value:
+ source:
+ sourceType: REALTIME_FUNDING
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000009
+ currency: USD
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456
+ lockedCurrencySide: RECEIVING
+ lockedCurrencyAmount: 10000
+ immediatelyExecute: true
+ description: Bitcoin reward payout!
responses:
- '200':
- description: Funds received successfully
+ '201':
+ description: |
+ Transfer quote created successfully. The response includes exchange rates,
+ fees, and transfer details. For transfers involving UMA addresses, payment
+ instructions are also included for execution through banking systems.
content:
application/json:
schema:
- $ref: '#/components/schemas/OutgoingTransaction'
+ $ref: '#/components/schemas/Quote'
+ example:
+ id: Quote:019542f5-b3e7-1d02-0000-000000000006
+ status: PENDING
+ createdAt: '2025-10-03T12:00:00Z'
+ expiresAt: '2025-10-03T12:05:00Z'
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ sendingCurrency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ receivingCurrency:
+ code: EUR
+ name: Euro
+ symbol: €
+ decimals: 2
+ totalSendingAmount: 10000
+ totalReceivingAmount: 9200
+ exchangeRate: 0.92
+ feesIncluded: 10
+ transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ '202':
+ description: |
+ Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Quote'
'400':
- description: Bad request
+ description: Bad request - Missing or invalid parameters
content:
application/json:
schema:
@@ -3865,121 +3695,172 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
+ '412':
+ description: Counterparty doesn't support UMA version
content:
application/json:
schema:
- $ref: '#/components/schemas/Error403'
- '404':
- description: Quote not found
+ $ref: '#/components/schemas/Error412'
+ '424':
+ description: Counterparty issue
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/uma/receive:
+ '501':
+ description: Not implemented
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ /quotes/{quoteId}/execute:
post:
- summary: Simulate payment send to test receiving an UMA payment
+ summary: Execute a quote
description: |
- Simulate sending payment from an sandbox uma address to a platform customer to test payment receive.
- This endpoint is only for the sandbox environment and will fail for production platforms/keys.
- operationId: sandboxReceive
+ Execute a quote by its ID. This endpoint initiates the transfer between
+ the source and destination accounts.
+
+ This endpoint can only be used for quotes with a `source` which is either an internal account,
+ or has direct pull functionality (e.g. ACH pull with an external account).
+
+ When the quote's `source` is an internal account of type `EMBEDDED_WALLET`,
+ the request must include a `Grid-Wallet-Signature` header. The header value
+ is the full Grid wallet signature built over the `payloadToSign` value from
+ the quote's `paymentInstructions[].accountOrWalletInfo` entry with the
+ session private key of a verified authentication credential on the source
+ Embedded Wallet.
+
+ Once executed, the quote cannot be cancelled and the transfer will be processed.
+ operationId: executeQuote
tags:
- - Sandbox
+ - Cross-Currency Transfers
security:
- BasicAuth: []
+ parameters:
+ - name: quoteId
+ in: path
+ required: true
+ description: The unique identifier of the quote to execute
+ schema:
+ type: string
+ example: Quote:019542f5-b3e7-1d02-0000-000000000001
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/SandboxUmaReceiveRequest'
+ $ref: '#/components/schemas/ExecuteQuoteRequest'
responses:
'200':
- description: Payment triggered successfully
+ description: |
+ Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers):
+
+ - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`).
+
+ - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`.
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingTransaction'
+ $ref: '#/components/schemas/Quote'
'400':
- description: Bad request
+ description: Bad request - Invalid quote ID or quote cannot be confirmed
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
+ '404':
+ description: Quote not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error403'
- '404':
- description: Sender or receiver not found
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: Conflict - Quote already confirmed, expired, or in invalid state
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/internal-accounts/{accountId}/fund:
+ /quotes/{quoteId}/authorize:
+ parameters:
+ - name: quoteId
+ in: path
+ description: The unique identifier of the quote whose SCA challenge is being authorized.
+ required: true
+ schema:
+ type: string
+ example: Quote:019542f5-b3e7-1d02-0000-000000000006
post:
- summary: Simulate funding an internal account
+ summary: Authorize a quote's SCA challenge
description: |
- Simulate receiving funds into an internal account in the sandbox environment. This is useful for testing scenarios where you need to add funds to a customer's or platform's internal account without going through a real bank transfer or following payment instructions.
- This endpoint is only for the sandbox environment and will fail for production platforms/keys.
- operationId: sandboxFundInternalAccount
+ Satisfy the Strong Customer Authentication challenge carried by a quote in
+ `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof.
+
+ This is used for realtime-funding quotes: the quote is returned with an
+ `scaChallenge` and **without** `paymentInstructions`; once authorized, the
+ quote advances and its `paymentInstructions` are populated so the customer
+ can fund the transfer.
+
+ As with all SCA, a quote may require more than one authorization: after
+ authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the
+ next `scaChallenge` — authorize that too, repeating until it advances (see
+ `ScaChallenge`).
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS code is always `123456`.
+ operationId: authorizeQuote
tags:
- - Sandbox
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: accountId
- in: path
- required: true
- description: The ID of the internal account to fund
- schema:
- type: string
- example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/SandboxFundRequest'
- examples:
- fundUSDAccount:
- summary: Fund USD internal account with $1,000
- value:
- amount: 100000
- fundBTCAccount:
- summary: Fund BTC internal account with 0.01 BTC
- value:
- amount: 1000000
+ $ref: '#/components/schemas/ScaAuthorization'
responses:
'200':
- description: Internal account funded successfully
+ description: Challenge authorized; the updated quote is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/InternalAccount'
+ $ref: '#/components/schemas/Quote'
'400':
- description: Bad request
+ description: Invalid or expired authorization proof
content:
application/json:
schema:
@@ -3990,180 +3871,166 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
+ '404':
+ description: Quote not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error403'
- '404':
- description: Internal account not found
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer, or the quote has no pending challenge to authorize.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
+ '429':
+ description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /uma-providers:
- get:
- summary: List available Counterparty Providers
+ /quotes/{quoteId}/authorize/resend:
+ parameters:
+ - name: quoteId
+ in: path
+ description: The unique identifier of the quote whose SCA challenge code should be re-sent.
+ required: true
+ schema:
+ type: string
+ example: Quote:019542f5-b3e7-1d02-0000-000000000006
+ post:
+ summary: Resend a quote's SCA challenge code
description: |
- Retrieve a list of available Counterparty Providers. The response includes basic information about each provider, such as its UMA address, name, and supported currencies.
- operationId: getAvailableUmaProviders
+ Re-send the one-time code for a realtime-funding quote in
+ `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The
+ existing challenge is reused — no new challenge is issued, and its
+ `scaChallenge.expiresAt` is **not** extended; once the challenge is past
+ `expiresAt` it can no longer be authorized.
+
+ Only meaningful for customers in a region where SCA is required (e.g. EU);
+ a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409.
+
+ In sandbox, the code is always `123456`.
+ operationId: resendQuoteScaCode
tags:
- - Available UMA Providers
- parameters:
- - in: query
- name: countryCode
- description: The alpha-2 representation of a country, as defined by the ISO 3166-1 standard.
- required: false
- schema:
- type: string
- example: US
- - in: query
- name: currencyCode
- description: The ISO 4217 currency code to filter providers by supported currency.
- required: false
- schema:
- type: string
- example: USD
- - in: query
- name: hasBlockedProviders
- description: Whether to include providers which are not on your allowlist in the response. By default the response will include blocked providers.
- required: false
- schema:
- type: boolean
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
- - name: sortOrder
- in: query
- description: Order to sort results in
- required: false
- schema:
- type: string
- enum:
- - asc
- - desc
- default: desc
+ - Strong Customer Authentication
security:
- BasicAuth: []
responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UmaProviderListResponse'
+ '204':
+ description: Code re-sent.
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /tokens:
- post:
- summary: Create a new API token
- description: Create a new API token to access the Grid APIs.
- operationId: createToken
- tags:
- - API Tokens
- security:
- - BasicAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ApiTokenCreateRequest'
- responses:
- '201':
- description: API token created successfully
+ '404':
+ description: Quote not found
content:
application/json:
schema:
- $ref: '#/components/schemas/ApiToken'
- '400':
- description: Bad request
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey).
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
+ $ref: '#/components/schemas/Error409'
+ '429':
+ description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /transactions:
get:
- summary: List tokens
+ summary: List transactions
description: |
- Retrieve a list of API tokens with optional filtering parameters. Returns all tokens that match
- the specified filters. If no filters are provided, returns all tokens (paginated).
- operationId: listTokens
+ Retrieve a paginated list of transactions with optional filtering.
+ The transactions can be filtered by customer ID, platform customer ID, UMA address,
+ date range, status, and transaction type.
+
+ Card transactions are included and identified by `type: CARD`. In Sandbox this is how
+ you discover a `CardTransaction` id after simulating an authorization — list the
+ transactions, take the card transaction's `id`, and pass it as the `cardTransactionId`
+ to the clearing and return simulate endpoints.
+ operationId: listTransactions
tags:
- - API Tokens
+ - Transactions
security:
- BasicAuth: []
parameters:
- - name: name
+ - name: customerId
in: query
- description: Filter by name of the token
+ description: Filter by system customer ID. To filter to transactions made on behalf of the platform, specify the platform ID as the customer ID.
required: false
schema:
type: string
- - name: createdAfter
+ - name: platformCustomerId
in: query
- description: Filter customers created after this timestamp (inclusive)
+ description: Filter by platform-specific customer ID
required: false
schema:
type: string
- format: date-time
- - name: createdBefore
+ - name: accountIdentifier
in: query
- description: Filter customers created before this timestamp (inclusive)
+ description: Filter by account identifier (matches either sender or receiver)
required: false
schema:
type: string
- format: date-time
- - name: updatedAfter
+ - name: senderAccountIdentifier
in: query
- description: Filter customers updated after this timestamp (inclusive)
+ description: Filter by sender account identifier
+ required: false
+ schema:
+ type: string
+ - name: receiverAccountIdentifier
+ in: query
+ description: Filter by receiver account identifier
+ required: false
+ schema:
+ type: string
+ - name: status
+ in: query
+ description: Filter by transaction status
+ required: false
+ schema:
+ $ref: '#/components/schemas/TransactionStatus'
+ - name: type
+ in: query
+ description: Filter by transaction type
+ required: false
+ schema:
+ $ref: '#/components/schemas/TransactionType'
+ - name: reference
+ in: query
+ description: Filter by reference
+ required: false
+ schema:
+ type: string
+ - name: startDate
+ in: query
+ description: Filter by start date (inclusive) in ISO 8601 format
required: false
schema:
type: string
format: date-time
- - name: updatedBefore
+ - name: endDate
in: query
- description: Filter customers updated before this timestamp (inclusive)
+ description: Filter by end date (inclusive) in ISO 8601 format
required: false
schema:
type: string
@@ -4183,13 +4050,23 @@ paths:
required: false
schema:
type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/TokenListResponse'
+ $ref: '#/components/schemas/TransactionListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -4208,20 +4085,20 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /tokens/{tokenId}:
+ /transactions/{transactionId}:
parameters:
- - name: tokenId
+ - name: transactionId
in: path
- description: System-generated unique token identifier
+ description: Unique identifier of the transaction
required: true
schema:
type: string
get:
- summary: Get API token by ID
- description: Retrieve an API token by their system-generated ID
- operationId: getTokenById
+ summary: Get transaction by ID
+ description: Retrieve detailed information about a specific transaction.
+ operationId: getTransactionById
tags:
- - API Tokens
+ - Transactions
security:
- BasicAuth: []
responses:
@@ -4230,7 +4107,7 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/ApiToken'
+ $ref: '#/components/schemas/TransactionOneOf'
'401':
description: Unauthorized
content:
@@ -4238,7 +4115,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Token not found
+ description: Transaction not found
content:
application/json:
schema:
@@ -4249,19 +4126,38 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- delete:
- summary: Delete API token by ID
- description: Delete an API token by their system-generated ID
- operationId: deleteTokenById
+ /transactions/{transactionId}/confirm:
+ post:
+ summary: Confirm receipt delivery
+ description: |
+ Confirm that the platform delivered the transaction receipt to its customer. This confirmation is only necessary when the platform is contractually required to send a receipt. If `receiptDeliveryConfirmedAt` is omitted, the confirmation time is set to the current server time. Calling this endpoint again for the same transaction updates the stored confirmation time.
+ operationId: confirmReceiptDelivery
tags:
- - API Tokens
+ - Transactions
security:
- BasicAuth: []
+ parameters:
+ - name: transactionId
+ in: path
+ description: Unique identifier of the transaction to confirm receipt delivery for
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ConfirmReceiptDeliveryRequest'
responses:
- '204':
- description: API token deleted successfully
+ '200':
+ description: Receipt delivery confirmation recorded successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TransactionOneOf'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -4273,7 +4169,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Token not found
+ description: Transaction not found
content:
application/json:
schema:
@@ -4284,368 +4180,168 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /internal-accounts/{id}:
- parameters:
- - name: id
- in: path
- description: The id of the internal account to update.
- required: true
- schema:
- type: string
- example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- patch:
- summary: Update internal account
+ /transactions/{transactionId}/approve:
+ post:
+ summary: Approve a pending incoming payment
description: |
- Update mutable fields on an internal account. Today this supports updating the wallet privacy setting for an Embedded Wallet internal account.
-
- Updating wallet privacy is a two-step signed-retry flow:
-
- 1. Call `PATCH /internal-accounts/{id}` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
-
- 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same update fields submitted in step 1. The signed retry returns `200` with the updated internal account.
- operationId: updateInternalAccount
+ Approve a pending incoming payment that was previously acknowledged with a 202 response.
+ This endpoint allows platforms to asynchronously approve payments after async processing.
+ operationId: approvePendingPayment
tags:
- - Internal Accounts
+ - Transactions
security:
- BasicAuth: []
parameters:
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ - name: transactionId
+ in: path
+ description: Unique identifier of the transaction to approve
+ required: true
schema:
type: string
- example: Request:019542f5-b3e7-1d02-0000-000000000010
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/InternalAccountUpdateRequest'
- examples:
- updateWalletPrivacy:
- summary: Update wallet privacy request (both steps)
- value:
- privateEnabled: true
+ $ref: '#/components/schemas/ApprovePaymentRequest'
responses:
'200':
- description: Signed retry accepted. Returns the updated internal account.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/InternalAccount'
- examples:
- enabled:
- summary: Wallet privacy enabled
- value:
- id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- type: EMBEDDED_WALLET
- status: ACTIVE
- balance:
- amount: 12550
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- totalBalance:
- amount: 12550
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- fundingPaymentInstructions: []
- privateEnabled: true
- createdAt: '2026-04-08T15:30:00Z'
- updatedAt: '2026-04-08T15:35:02Z'
- '202':
- description: Challenge issued. The response contains `payloadToSign` (which binds the submitted update fields) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry.
+ description: Payment approved successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/SignedRequestChallenge'
- examples:
- challenge:
- summary: Internal account update challenge
- value:
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"encoding":"PAYLOAD_ENCODING_HEXADECIMAL","hashFunction":"HASH_FUNCTION_NO_OP","payload":"9f3b...","signWith":"sp1q..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2"}'
- requestId: Request:019542f5-b3e7-1d02-0000-000000000010
- expiresAt: '2026-04-08T15:35:00Z'
+ $ref: '#/components/schemas/IncomingTransaction'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters or payment cannot be approved
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending internal account update challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry body does not match the update fields bound into `payloadToSign` on the initial call.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Internal account not found
+ description: Transaction not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: Conflict - Payment is not in a pending state or has already been processed or timed out.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /internal-accounts/{id}/export:
+ /transactions/{transactionId}/reject:
post:
- summary: Export internal account wallet credentials
+ summary: Reject a pending incoming payment
description: |
- Export the wallet credentials of an Embedded Wallet internal account. The returned wallet credentials are HPKE-encrypted to the `clientPublicKey` supplied in the request body.
-
- Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session):
-
- 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`.
-
- 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key.
-
- The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against.
- operationId: exportInternalAccount
+ Reject a pending incoming payment that was previously acknowledged with a 202 response.
+ This endpoint allows platforms to asynchronously reject payments after additional processing.
+ operationId: rejectPendingPayment
tags:
- - Internal Accounts
+ - Transactions
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: transactionId
in: path
- description: The id of the internal account to export.
+ description: Unique identifier of the transaction to reject
required: true
schema:
type: string
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/InternalAccountExportRequest'
- examples:
- export:
- summary: Export request (both steps)
- value:
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ $ref: '#/components/schemas/RejectPaymentRequest'
responses:
'200':
- description: Signed retry accepted. Returns the encrypted wallet credentials.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/InternalAccountExportResponse'
- '202':
- description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry.
+ description: Payment rejected successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/SignedRequestChallenge'
+ $ref: '#/components/schemas/IncomingTransaction'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters or payment cannot be rejected
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending export challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Internal account not found
+ description: Transaction not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: Conflict - Payment is not in a pending state or has already been processed or timed out.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/credentials:
+ /crypto/estimate-withdrawal-fee:
post:
- summary: Create an authentication credential
+ summary: Estimate crypto withdrawal fee
description: |
- Register an authentication credential for an Embedded Wallet customer.
-
- Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered.
-
- Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.
- operationId: createAuthCredential
+ Estimate the network and application fees for a cryptocurrency withdrawal from a crypto internal account to an external blockchain address. Use this to show fee information to customers before they initiate a withdrawal.
+ operationId: estimateCryptoWithdrawalFee
tags:
- - Embedded Wallet Auth
+ - Cross-Currency Transfers
security:
- BasicAuth: []
- parameters:
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required on the signed retry.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering a credential; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf'
+ $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeRequest'
examples:
- emailOtp:
- summary: Add an email OTP credential
- value:
- type: EMAIL_OTP
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- smsOtp:
- summary: Add an SMS OTP credential
- value:
- type: SMS_OTP
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- oauth:
- summary: Add an OAuth credential
+ estimateUSDCWithdrawal:
+ summary: Estimate fee for USDC withdrawal on Solana
value:
- type: OAUTH
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
- passkey:
- summary: Add a passkey credential
- value:
- type: PASSKEY
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- nickname: iPhone Face-ID
- challenge: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx
- attestation:
- credentialId: AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY
- clientDataJson: eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0
- attestationObject: o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow
- transports:
- - internal
- - hybrid
+ internalAccountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ currency: USDC
+ cryptoNetwork: SOLANA
+ amount: 1000000
+ destinationAddress: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU
responses:
- '201':
- description: Authentication credential created successfully. The body is the created `AuthMethod`. For `EMAIL_OTP`, the nickname is the customer email tied to the internal account; for `SMS_OTP`, it is the customer phone number. OTP responses that trigger a secure OTP challenge carry `otpEncryptionTargetBundle` — the HPKE target bundle the client uses to encrypt the OTP attempt on the subsequent `POST /auth/credentials/{id}/verify`. First-time EMAIL_OTP wallet bootstrap responses may omit that bundle; if it is absent, call `POST /auth/credentials/{id}/challenge` for the new credential to issue a fresh OTP and receive `otpEncryptionTargetBundle` before verifying. For `PASSKEY`, the credential must be authenticated for the first time via `POST /auth/credentials/{id}/challenge` followed by `POST /auth/credentials/{id}/verify` to produce a session — there is no inline authentication challenge on the registration response.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthMethodResponse'
- examples:
- emailOtp:
- summary: Email OTP credential created
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: EMAIL_OTP
- nickname: example@lightspark.com
- otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- smsOtp:
- summary: SMS OTP credential created
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: SMS_OTP
- nickname: '+14155550123'
- otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- oauth:
- summary: OAuth credential created
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: OAUTH
- nickname: example@lightspark.com
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- passkey:
- summary: Passkey credential created
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: PASSKEY
- nickname: iPhone Face-ID
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- '202':
- description: Challenge issued. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry.
+ '200':
+ description: Fee estimation returned successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthSignedRequestChallenge'
- examples:
- emailOtp:
- summary: Additional email OTP credential challenge
- value:
- type: EMAIL_OTP
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userEmail":"jane@example.com","userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_EMAIL"}'
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
- smsOtp:
- summary: Additional SMS OTP credential challenge
- value:
- type: SMS_OTP
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F...","userPhoneNumber":"+14155550123"},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER"}'
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
- oauth:
- summary: Additional OAuth credential challenge
- value:
- type: OAUTH
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"oauthProviders":[{"oidcToken":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...","providerName":"Google"}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS"}'
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
- passkey:
- summary: Additional passkey credential challenge
- value:
- type: PASSKEY
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"authenticators":[{"attestation":{"attestationObject":"o2NmbXRk...","clientDataJson":"eyJjaGFsbGVuZ2UiOiJBcktRa...","credentialId":"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY"},"authenticatorName":"iPhone Face-ID","challenge":"ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx","transports":["internal","hybrid"]}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2"}'
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
+ $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeResponse'
'400':
- description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `SMS_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an SMS OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer.
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration.
+ description: Unauthorized
content:
application/json:
schema:
@@ -4662,62 +4358,24 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- get:
- summary: List authentication credentials
- description: |-
- Retrieve all authentication credentials registered on an Embedded Wallet internal account.
-
- The response is not paginated: an internal account is expected to have a small, bounded number of credentials (typically 1–5), so all results are returned inline. Additional per-credential detail (such as active session expiry) is available on `GET /auth/sessions`.
- operationId: listAuthCredentials
+ /sandbox/webhooks/test:
+ post:
+ summary: Send a test webhook
+ description: Send a test webhook to the configured endpoint
+ operationId: sendTestWebhook
tags:
- - Embedded Wallet Auth
+ - Sandbox
security:
- BasicAuth: []
- parameters:
- - name: accountId
- in: query
- description: Internal account id whose authentication credentials to list.
- required: true
- schema:
- type: string
- example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
responses:
'200':
- description: Authentication credentials registered on the internal account. Returns an empty `data` array when the internal account has no credentials or when `accountId` does not match any internal account visible to the caller.
+ description: Webhook delivered successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthCredentialListResponse'
- examples:
- multipleCredentials:
- summary: Internal account with multiple authentication credentials
- value:
- data:
- - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: EMAIL_OTP
- nickname: example@lightspark.com
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000004
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: OAUTH
- nickname: example@lightspark.com
- createdAt: '2026-04-08T15:35:00Z'
- updatedAt: '2026-04-08T15:35:00Z'
- - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000003
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: PASSKEY
- credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
- nickname: iPhone Face-ID
- createdAt: '2026-04-09T10:15:00Z'
- updatedAt: '2026-04-09T10:15:00Z'
- empty:
- summary: No credentials registered on the account
- value:
- data: []
+ $ref: '#/components/schemas/TestWebhookResponse'
'400':
- description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier.
+ description: Bad request - Webhook delivery failed
content:
application/json:
schema:
@@ -4734,282 +4392,168 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/credentials/{id}:
- delete:
- summary: Revoke an authentication credential
+ /customers/bulk/csv:
+ post:
+ summary: Upload customers via CSV file
description: |
- Revoke an authentication credential on an Embedded Wallet internal account.
+ Upload a CSV file containing customer information for bulk creation. The CSV file should follow
+ a specific format with required and optional columns based on customer type.
- Revocation is a two-step flow because it must be authorized by a session on a *different* credential on the same internal account:
+ ### CSV Format
+ The CSV file should have the following columns:
- 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+ Required columns for all customers:
+ - umaAddress: The customer's UMA address (e.g., $john.doe@uma.domain.com)
+ - platformCustomerId: Your platform's unique identifier for the customer
+ - customerType: Either "INDIVIDUAL" or "BUSINESS"
- 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`.
+ Required columns for individual customers:
+ - fullName: Individual's full name
+ - birthDate: Date of birth in YYYY-MM-DD format
+ - addressLine1: Street address line 1
+ - city: City
+ - state: State/Province/Region
+ - postalCode: Postal/ZIP code
+ - country: Country code (ISO 3166-1 alpha-2)
- The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it.
- operationId: revokeAuthCredential
+ Required columns for business customers:
+ - businessLegalName: Legal name of the business
+ - addressLine1: Street address line 1
+ - city: City
+ - state: State/Province/Region
+ - postalCode: Postal/ZIP code
+ - country: Country code (ISO 3166-1 alpha-2)
+
+ Optional columns for all customers:
+ - addressLine2: Street address line 2
+ - platformAccountId: Your platform's identifier for the bank account
+ - description: Optional description for the customer
+
+ Optional columns for individual customers:
+ - email: Customer's email address
+
+ Optional columns for business customers:
+ - businessRegistrationNumber: Business registration number
+ - businessTaxId: Tax identification number
+
+ ### Example CSV
+ ```csv
+ umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,platformAccountId,businessLegalName
+ john.doe@uma.domain.com,customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US
+ acme@uma.domain.com,biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US
+ ```
+
+ The upload process is asynchronous and will return a job ID that can be used to track progress.
+ You can monitor the job status using the `/customers/bulk/jobs/{jobId}` endpoint.
+ operationId: uploadCustomersCsv
tags:
- - Embedded Wallet Auth
+ - Customers
security:
- BasicAuth: []
- parameters:
- - name: id
- in: path
- description: The id of the authentication credential to revoke (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
- required: true
- schema:
- type: string
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required:
+ - file
+ properties:
+ file:
+ type: string
+ format: binary
+ description: CSV file containing customer information
responses:
'202':
- description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSignedRequestChallenge'
- '204':
- description: Authentication credential revoked successfully.
- '400':
- description: Bad request. Also returned when the target internal account has only a single authentication credential, which cannot be revoked via this endpoint.
+ description: CSV upload accepted for processing
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/BulkCustomerImportJobAccepted'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this credential, or when the `Request-Id` does not match an unexpired pending challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Authentication credential not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/credentials/{id}/verify:
- post:
- summary: Verify an authentication credential
+ /customers/bulk/jobs/{jobId}:
+ get:
+ summary: Get bulk import job status
description: |
- Complete the verification step for a previously created authentication credential and issue a session.
-
- For `EMAIL_OTP` and `SMS_OTP` credentials, submit the `encryptedOtpBundle` produced by HPKE-encrypting `{otp_code, public_key}` under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued. The server is a pass-through and never sees the plaintext OTP code. On success the response is `202` with a `payloadToSign` carrying the `verificationToken` bound to the client's TEK public key — sign that token with the matching TEK private key, then retry the same request with the full stamp in `Grid-Wallet-Signature` and the `requestId` echoed in `Request-Id`. The signed retry returns `200` with the issued `AuthSession`. The TEK public key becomes the session API key on successful completion.
- In sandbox mode, the OTP flow runs real HPKE end-to-end against a sandbox enclave keypair — clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code (`"000000"`) the user "receives" instead of a real email or SMS delivery.
-
- For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request.
+ Retrieve the current status and results of a bulk customer import job. This endpoint can be used
+ to track the progress of both CSV uploads.
- On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires.
- operationId: verifyAuthCredential
+ The response includes:
+ - Overall job status
+ - Progress statistics
+ - Detailed error information for failed entries
+ - Completion timestamp when finished
+ operationId: getBulkCustomerImportJob
tags:
- - Embedded Wallet Auth
+ - Customers
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: jobId
in: path
- description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
+ description: ID of the bulk import job to retrieve
required: true
schema:
type: string
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the TEK (Target Encryption Key) keypair the client generated for this login. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification. Not used by `OAUTH` or `PASSKEY` verification, which complete in a single call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response from this endpoint, echoed back exactly here so the server can correlate the signed retry with the issued challenge. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification; must be paired with `Grid-Wallet-Signature`. For `PASSKEY` verification, the `requestId` issued from `POST /auth/credentials/{id}/challenge` is echoed here instead so the server can correlate the assertion with the pending challenge.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf'
- examples:
- emailOtp:
- summary: Verify an email OTP credential (first leg)
- value:
- type: EMAIL_OTP
- encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
- smsOtp:
- summary: Verify an SMS OTP credential (first leg)
- value:
- type: SMS_OTP
- encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
- emailOtpSignedRetry:
- summary: Signed retry completing an email OTP verification
- description: Same request body as the first leg, plus the `Grid-Wallet-Signature` and `Request-Id` headers carrying the stamp over the `verificationToken` and the `requestId` from the prior `202` response.
- value:
- type: EMAIL_OTP
- encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
- oauth:
- summary: Verify an OAuth credential
- value:
- type: OAUTH
- oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
- passkey:
- summary: Verify a passkey credential
- value:
- type: PASSKEY
- assertion:
- credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
- clientDataJson: eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0
- authenticatorData: PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA
- signature: MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6jhd45bDx92wjXKs900
responses:
'200':
- description: Authentication credential verified and session issued
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSession'
- '202':
- description: Verification challenge issued. Returned only for OTP credentials, on the first leg of the secure OTP login flow. Build an API-key stamp over `payloadToSign` (the `verificationToken`) with the TEK keypair the client generated for this login, then resubmit the same request with that full stamp as `Grid-Wallet-Signature` and `requestId` echoed as `Request-Id` to receive the issued session on the signed retry.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSignedRequestChallenge'
- examples:
- emailOtp:
- summary: Email OTP verification challenge (sign and retry)
- value:
- type: EMAIL_OTP
- payloadToSign: eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ.eyJzdWIiOiJUWnk2NkVPa1RGYTd2NkpXZ0VxaVgyZGFXOENXc2pMQzVDVU9aRUlGY3hzIiwiaWF0IjoxNzc5NDA3MjIxLCJleHAiOjE3Nzk0MTA4MjF9.gKX9MWYGkw8Y55bgzsgrRftvUHFruIe8yu0w9Kpjp5qnrZnXcTV71WVoltGPsr015IY_oRTOkIFLHmiGNG9zBw
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
- '400':
- description: Bad request
+ description: Job status retrieved successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/BulkCustomerImportJob'
'401':
- description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP` or `SMS_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. For OTP signed retries, returned when `Grid-Wallet-Signature` is missing, malformed, signed by a public key that does not match the one bound into the `verificationToken`, or when `Request-Id` does not match an unexpired pending verification challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Authentication credential not found
+ description: Job not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '429':
- description: Too many requests. Returned with `RATE_LIMITED` when verification attempts for this credential happen too frequently (for example, repeated bad OTPs or rapid-fire reauthentication retries). Clients should back off and retry after the interval indicated by the `Retry-After` response header.
- headers:
- Retry-After:
- description: Number of seconds to wait before retrying the request.
- schema:
- type: integer
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/credentials/{id}/challenge:
+ /invitations:
post:
- summary: Re-issue an authentication credential challenge
+ summary: Create an UMA invitation
description: |
- Re-issue the challenge for an existing authentication credential.
-
- For `EMAIL_OTP` and `SMS_OTP` credentials, this triggers a new one-time password to the contact on file and returns a fresh `otpEncryptionTargetBundle` for the client to HPKE-encrypt the OTP attempt against. After the user receives the new OTP, build the `encryptedOtpBundle` under the new target bundle and call `POST /auth/credentials/{id}/verify` to begin the secure OTP login flow.
-
- `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`.
-
- For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session.
- operationId: challengeAuthCredential
+ Create an UMA invitation from a given platform customer.
+ operationId: createInvitation
tags:
- - Embedded Wallet Auth
+ - Invitations
security:
- BasicAuth: []
- parameters:
- - name: id
- in: path
- description: The id of the authentication credential to re-challenge (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
- required: true
- schema:
- type: string
requestBody:
- description: Request body. Required when re-challenging a `PASSKEY` credential (must carry `clientPublicKey`). Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient — the OTP is delivered out-of-band. OAuth credentials do not use this endpoint.
- required: false
+ required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthCredentialChallengeRequest'
- examples:
- passkey:
- summary: Re-challenge a passkey credential
- value:
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
- emailOtp:
- summary: Re-challenge an email-OTP credential (empty body)
- value: {}
- smsOtp:
- summary: Re-challenge an SMS-OTP credential (empty body)
- value: {}
+ $ref: '#/components/schemas/UmaInvitationCreateRequest'
responses:
- '200':
- description: Challenge re-issued for the authentication credential. For `EMAIL_OTP` and `SMS_OTP` the body is a plain `AuthMethod` and a new OTP has been sent. For `PASSKEY` the body is a `PasskeyAuthChallenge` carrying the passkey `credentialId`, freshly issued `challenge`, `requestId`, and `expiresAt` required to complete reauthentication via `POST /auth/credentials/{id}/verify`.
+ '201':
+ description: Invitation created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthCredentialResponseOneOf'
- examples:
- emailOtp:
- summary: Email OTP challenge re-issued
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: EMAIL_OTP
- nickname: example@lightspark.com
- otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:35:00Z'
- passkey:
- summary: Passkey reauthentication challenge issued
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: PASSKEY
- credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
- nickname: iPhone Face-ID
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:35:00Z'
- challenge: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
+ $ref: '#/components/schemas/UmaInvitation'
'400':
description: Bad request
content:
@@ -5022,120 +4566,91 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Authentication credential not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '429':
- description: Too many requests. Returned with `RATE_LIMITED` when challenge re-issues are requested more frequently than the credential challenge rate limit allows. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
- headers:
- Retry-After:
- description: Number of seconds to wait before retrying the request.
- schema:
- type: integer
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/sessions:
+ /invitations/{invitationCode}:
get:
- summary: List active sessions
- description: |-
- Retrieve all active authentication sessions on an Embedded Wallet internal account. A session is created each time a credential is verified via `POST /auth/credentials/{id}/verify`, and remains active until its `expiresAt` passes or it is revoked via `DELETE /auth/sessions/{id}`.
-
- The response is not paginated: an internal account is expected to have a small, bounded number of concurrent sessions (one per signed-in device, typically 1–4), so all results are returned inline.
- operationId: listAuthSessions
+ summary: Get an UMA invitation by code
+ description: |
+ Retrieve details about an UMA invitation by its invitation code.
+ operationId: getInvitation
tags:
- - Embedded Wallet Auth
+ - Invitations
security:
- BasicAuth: []
parameters:
- - name: accountId
- in: query
- description: Internal account id whose sessions to list.
+ - name: invitationCode
+ in: path
+ description: The code of the invitation to get
required: true
schema:
type: string
- example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
responses:
'200':
- description: Active authentication sessions on the internal account. Returns an empty `data` array when the internal account has no active sessions or when `accountId` does not match any internal account visible to the caller.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SessionListResponse'
- '400':
- description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier.
+ description: Invitation retrieved successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/UmaInvitation'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
+ '404':
+ description: Invitation not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/sessions/{id}:
- delete:
- summary: Revoke an authentication session
+ /invitations/{invitationCode}/claim:
+ post:
+ summary: Claim an UMA invitation
description: |
- Revoke an authentication session on an Embedded Wallet internal account. Revocation is a two-step signed-retry flow:
-
- 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+ Claim an UMA invitation by associating it with an invitee UMA address.
- 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`.
+ When an invitation is successfully claimed:
+ 1. The invitation status changes from PENDING to CLAIMED
+ 2. The invitee UMA address is associated with the invitation
+ 3. An INVITATION_CLAIMED webhook is triggered to notify the platform that created the invitation
- Sessions also expire on their own. `404` is returned whenever the `id` does not match an active session — whether the session was never issued, was already revoked by a prior call, or has expired past its `expiresAt`. The response code reflects the resource state, not an error in the client's flow: re-revoking an already-revoked or expired session is safe and idempotent at the user intent level.
- operationId: revokeAuthSession
+ This endpoint allows customers to accept invitations sent to them by other UMA customers.
+ operationId: claimInvitation
tags:
- - Embedded Wallet Auth
+ - Invitations
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: invitationCode
in: path
- description: The id of the session to revoke.
+ description: The code of the invitation to claim
required: true
schema:
type: string
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UmaInvitationClaimRequest'
responses:
- '202':
- description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry.
+ '200':
+ description: Invitation claimed successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthSignedRequestChallenge'
- '204':
- description: Session revoked successfully.
+ $ref: '#/components/schemas/UmaInvitation'
'400':
description: Bad request
content:
@@ -5143,13 +4658,13 @@ paths:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this session, or when the `Request-Id` does not match an unexpired pending challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Session not found
+ description: Invitation not found
content:
application/json:
schema:
@@ -5160,101 +4675,58 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/sessions/{id}/refresh:
+ /invitations/{invitationCode}/cancel:
post:
- summary: Refresh an authentication session
+ summary: Cancel an UMA invitation
description: |
- Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow:
-
- 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+ Cancel a pending UMA invitation. Only the inviter or platform can cancel an invitation.
- 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key.
+ When an invitation is cancelled:
+ 1. The invitation status changes from PENDING to CANCELLED
+ 2. The invitation can no longer be claimed
+ 3. The invitation URL will show as cancelled when accessed
- The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead.
- operationId: refreshAuthSession
+ Only pending invitations can be cancelled. Attempting to cancel an invitation
+ that is already claimed, expired, or cancelled will result in an error.
+ operationId: cancelInvitation
tags:
- - Embedded Wallet Auth
+ - Invitations
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: invitationCode
in: path
- description: The id of the active session to refresh.
+ description: The code of the invitation to cancel
required: true
schema:
type: string
- example: Session:019542f5-b3e7-1d02-0000-000000000003
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the current session API keypair. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in the prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:019542f5-b3e7-1d02-0000-000000000010
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSessionRefreshRequest'
- examples:
- refresh:
- summary: Refresh an active session
- value:
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
responses:
- '201':
- description: New authentication session created successfully.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSession'
- examples:
- session:
- summary: Refreshed authentication session
- value:
- id: Session:019542f5-b3e7-1d02-0000-000000000011
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: EMAIL_OTP
- encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
- nickname: example@lightspark.com
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:35:00Z'
- expiresAt: '2026-04-08T15:50:00Z'
- '202':
- description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the current session API keypair, then echo `requestId` on the signed retry.
+ '200':
+ description: Invitation cancelled successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/SignedRequestChallenge'
- examples:
- challenge:
- summary: Session refresh challenge
- value:
- payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}'
- requestId: Request:019542f5-b3e7-1d02-0000-000000000010
- expiresAt: '2026-04-08T15:35:00Z'
+ $ref: '#/components/schemas/UmaInvitation'
'400':
- description: Bad request
+ description: Bad request - Invitation cannot be cancelled (already claimed, expired, or cancelled)
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the `BasicAuth` credentials are missing or invalid, when the target session is no longer active and cannot be used for refresh, when the signed retry omits `Grid-Wallet-Signature`, when the provided signature is malformed or does not match the pending refresh challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - Only the platform which created the invitation can cancel it
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
'404':
- description: Session not found
+ description: Invitation not found
content:
application/json:
schema:
@@ -5265,80 +4737,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/delegated-keys:
+ /sandbox/send:
post:
- summary: Create a delegated signing key
+ summary: Simulate sending funds
description: |
- Delegate Spark token-transaction signing authority for a card funding source backed by an Embedded Wallet internal account to a Grid-custodied P-256 API key. Grid uses the requested card and internal account to identify the wallet funding source, generates the keypair server-side, creates an isolated signer identity holding the public key, then policies granting that identity signing and self-revocation authority. The private key is custodied by Grid and never returned. Both activities must be authorized by the wallet owner, so creation is a three-leg signed-retry flow:
-
- 1. Call `POST /auth/delegated-keys` with no signature headers. Grid generates the delegated keypair and the response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
-
- 2. Use the session API keypair of a verified credential on the requested Embedded Wallet internal account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The response is a second `202` with a new `payloadToSign`, `requestId`, and `expiresAt`.
-
- 3. Stamp the new `payloadToSign` with the same session keypair and retry once more with the new `Request-Id`. The signed retry returns `201` with the created `DelegatedKey` in `ACTIVE` status.
-
- The same request body must be sent on all three legs. A flow abandoned after the second leg leaves the key in `PENDING` status: the signer identity exists but holds no policies, so it cannot sign or revoke itself. Abandoned `PENDING` keys do not block creating another delegated key. After activation, Grid uses the custodied key to authorize signing for the card's Embedded Wallet funding account in place of a session keypair; the platform never handles the key material.
-
- Each card funding source may have at most one `ACTIVE` delegated key for its Embedded Wallet funding account; revoke the existing active key before creating a new one. A delegated key authorizes raw-payload signing for the wallet and cannot be scoped to amounts or recipients by the public API. Revoke it with `DELETE /auth/delegated-keys/{id}` when no longer needed.
- operationId: createDelegatedKey
+ Simulate sending funds to the bank account as instructed in the quote.
+ This endpoint is only for the sandbox environment and will fail for production platforms/keys.
+ operationId: sandboxSend
tags:
- - Embedded Wallet Auth
+ - Sandbox
security:
- BasicAuth: []
- parameters:
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified credential on the same internal account. Required on the signed retries; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in the prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retries; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/DelegatedKeyCreateRequest'
- examples:
- create:
- summary: Delegate signing to a Grid-custodied key
- value:
- cardId: Card:019542f5-b3e7-1d02-0000-000000000010
- internalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- nickname: Card payments key
+ $ref: '#/components/schemas/SandboxSendRequest'
responses:
- '201':
- description: Delegated key created and policy granted. The key is `ACTIVE` and Grid may use it to stamp card-payment quote executions for this card funding source's Embedded Wallet funding account.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/DelegatedKey'
- examples:
- delegatedKey:
- summary: Active delegated key
- value:
- id: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
- cardId: Card:019542f5-b3e7-1d02-0000-000000000010
- fundingSourceId: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- publicKey: 02a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90
- nickname: Card payments key
- status: ACTIVE
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:42Z'
- '202':
- description: Challenge issued for the next leg. Stamp `payloadToSign` and retry the same request with `Grid-Wallet-Signature` and `Request-Id`.
+ '200':
+ description: Funds received successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/DelegatedKeySignedRequestChallenge'
+ $ref: '#/components/schemas/OutgoingTransaction'
'400':
description: Bad request
content:
@@ -5346,59 +4768,53 @@ paths:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing on a retry, malformed, or does not match the pending challenge, or when the `Request-Id` does not match an unexpired pending challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Card, card funding source, or Embedded Wallet funding account not found
+ '403':
+ description: Forbidden - request was made with a production platform token
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: An `ACTIVE` delegated key already exists for this card funding source. Revoke it with `DELETE /auth/delegated-keys/{id}` before creating a new one.
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Quote not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- get:
- summary: List delegated signing keys
- description: List delegated signing keys for an Embedded Wallet internal account, a card funding source, or both, including `PENDING` keys (user created but policy leg never completed) and `REVOKED` keys. At least one of `accountId` or `fundingSourceId` must be supplied.
- operationId: listDelegatedKeys
+ /sandbox/uma/receive:
+ post:
+ summary: Simulate payment send to test receiving an UMA payment
+ description: |
+ Simulate sending payment from an sandbox uma address to a platform customer to test payment receive.
+ This endpoint is only for the sandbox environment and will fail for production platforms/keys.
+ operationId: sandboxReceive
tags:
- - Embedded Wallet Auth
+ - Sandbox
security:
- BasicAuth: []
- parameters:
- - name: accountId
- in: query
- required: false
- description: The id of the internal account whose delegated keys to list.
- schema:
- type: string
- example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- - name: fundingSourceId
- in: query
- required: false
- description: The id of the card funding source whose delegated keys to list.
- schema:
- type: string
- example: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxUmaReceiveRequest'
responses:
'200':
- description: Delegated keys matching the supplied filters. Returns an empty `data` array when no matching delegated keys are visible to the caller.
+ description: Payment triggered successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/DelegatedKeyListResponse'
+ $ref: '#/components/schemas/IncomingTransaction'
'400':
description: Bad request
content:
@@ -5411,44 +4827,85 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Sender or receiver not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/delegated-keys/{id}:
- get:
- summary: Get a delegated signing key
- description: Retrieve a delegated signing key by its system-generated id.
- operationId: getDelegatedKeyById
+ /sandbox/internal-accounts/{accountId}/fund:
+ post:
+ summary: Simulate funding an internal account
+ description: |
+ Simulate receiving funds into an internal account in the sandbox environment. This is useful for testing scenarios where you need to add funds to a customer's or platform's internal account without going through a real bank transfer or following payment instructions.
+ This endpoint is only for the sandbox environment and will fail for production platforms/keys.
+ operationId: sandboxFundInternalAccount
tags:
- - Embedded Wallet Auth
+ - Sandbox
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: accountId
in: path
- description: The id of the delegated key to retrieve (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys` or `GET /auth/delegated-keys`).
required: true
+ description: The ID of the internal account to fund
schema:
type: string
- example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
+ example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxFundRequest'
+ examples:
+ fundUSDAccount:
+ summary: Fund USD internal account with $1,000
+ value:
+ amount: 100000
+ fundBTCAccount:
+ summary: Fund BTC internal account with 0.01 BTC
+ value:
+ amount: 1000000
responses:
'200':
- description: Successful operation
+ description: Internal account funded successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/DelegatedKey'
+ $ref: '#/components/schemas/InternalAccount'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
'404':
- description: Delegated key not found
+ description: Internal account not found
content:
application/json:
schema:
@@ -5459,60 +4916,88 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- delete:
- summary: Revoke a delegated signing key
+ /uma-providers:
+ get:
+ summary: List available Counterparty Providers
description: |
- Revoke an `ACTIVE` delegated signing key. Grid uses the custodied delegated key to authorize deleting its own signer identity. Deleting the identity also removes its API key, after which the delegated key can no longer sign. The response is `204` when revocation completes.
-
- The underlying signing policies are left in place. Their consensus references the now-deleted signer identity, so they can never authorize anything, and deleting them is unnecessary for correctness or security.
- operationId: revokeDelegatedKey
+ Retrieve a list of available Counterparty Providers. The response includes basic information about each provider, such as its UMA address, name, and supported currencies.
+ operationId: getAvailableUmaProviders
tags:
- - Embedded Wallet Auth
- security:
- - BasicAuth: []
+ - Available UMA Providers
parameters:
- - name: id
- in: path
- description: The id of the delegated key to revoke (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys`).
- required: true
+ - in: query
+ name: countryCode
+ description: The alpha-2 representation of a country, as defined by the ISO 3166-1 standard.
+ required: false
schema:
type: string
- example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
+ example: US
+ - in: query
+ name: currencyCode
+ description: The ISO 4217 currency code to filter providers by supported currency.
+ required: false
+ schema:
+ type: string
+ example: USD
+ - in: query
+ name: hasBlockedProviders
+ description: Whether to include providers which are not on your allowlist in the response. By default the response will include blocked providers.
+ required: false
+ schema:
+ type: boolean
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
+ security:
+ - BasicAuth: []
responses:
- '204':
- description: Delegated key revoked. The key can no longer authorize signing.
- '400':
- description: Bad request. Returned when the delegated key has already been revoked or is not `ACTIVE`.
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/UmaProviderListResponse'
'401':
- description: Unauthorized.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Delegated key not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents:
+ /tokens:
post:
- summary: Create an agent
- description: |
- Create a new agent with a specified policy. Returns the created agent and a device code that must be redeemed by the agent software to complete installation.
- operationId: createAgent
+ summary: Create a new API token
+ description: Create a new API token to access the Grid APIs.
+ operationId: createToken
tags:
- - Agent Management
+ - API Tokens
security:
- BasicAuth: []
requestBody:
@@ -5520,14 +5005,14 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentCreateRequest'
+ $ref: '#/components/schemas/ApiTokenCreateRequest'
responses:
'201':
- description: Agent created successfully
+ description: API token created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentCreateResponse'
+ $ref: '#/components/schemas/ApiToken'
'400':
description: Bad request
content:
@@ -5547,56 +5032,46 @@ paths:
schema:
$ref: '#/components/schemas/Error500'
get:
- summary: List agents
- description: Retrieve a paginated list of agents for the authenticated platform.
- operationId: listAgents
+ summary: List tokens
+ description: |
+ Retrieve a list of API tokens with optional filtering parameters. Returns all tokens that match
+ the specified filters. If no filters are provided, returns all tokens (paginated).
+ operationId: listTokens
tags:
- - Agent Management
+ - API Tokens
security:
- BasicAuth: []
parameters:
- - name: customerId
+ - name: name
in: query
- description: Filter by customer ID
+ description: Filter by name of the token
required: false
schema:
type: string
- - name: isPaused
- in: query
- description: Filter by paused status
- required: false
- schema:
- type: boolean
- - name: isConnected
- in: query
- description: Filter by connection status (whether the device code has been redeemed)
- required: false
- schema:
- type: boolean
- name: createdAfter
in: query
- description: Filter agents created after this timestamp (inclusive)
+ description: Filter customers created after this timestamp (inclusive)
required: false
schema:
type: string
format: date-time
- name: createdBefore
in: query
- description: Filter agents created before this timestamp (inclusive)
+ description: Filter customers created before this timestamp (inclusive)
required: false
schema:
type: string
format: date-time
- name: updatedAfter
in: query
- description: Filter agents updated after this timestamp (inclusive)
+ description: Filter customers updated after this timestamp (inclusive)
required: false
schema:
type: string
format: date-time
- name: updatedBefore
in: query
- description: Filter agents updated before this timestamp (inclusive)
+ description: Filter customers updated before this timestamp (inclusive)
required: false
schema:
type: string
@@ -5622,7 +5097,7 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentListResponse'
+ $ref: '#/components/schemas/TokenListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -5641,153 +5116,192 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/approvals:
+ /tokens/{tokenId}:
+ parameters:
+ - name: tokenId
+ in: path
+ description: System-generated unique token identifier
+ required: true
+ schema:
+ type: string
get:
- summary: List agent transaction approval requests
- description: |
- Retrieve a paginated list of agent actions that require platform approval. Filter by `agentId` or `customerId` to scope results to a specific agent or customer. Approve or reject individual actions via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`.
- operationId: listAgentApprovals
+ summary: Get API token by ID
+ description: Retrieve an API token by their system-generated ID
+ operationId: getTokenById
tags:
- - Agent Management
+ - API Tokens
security:
- BasicAuth: []
- parameters:
- - name: agentId
- in: query
- description: Filter by agent ID
- required: false
- schema:
- type: string
- - name: customerId
- in: query
- description: Filter by customer ID
- required: false
- schema:
- type: string
- - name: startDate
- in: query
- description: Filter by start date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: endDate
- in: query
- description: Filter by end date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
- - name: sortOrder
- in: query
- description: Order to sort results in
- required: false
- schema:
- type: string
- enum:
- - asc
- - desc
- default: desc
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentActionListResponse'
- '400':
- description: Bad request - Invalid parameters
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/ApiToken'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Token not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me:
- get:
- summary: Get current agent
- description: |
- Retrieve the authenticated agent's own profile, policy, and current usage. This endpoint is called by the agent software itself using its own credentials (obtained via device code redemption) rather than platform credentials.
- operationId: getAgentMe
+ delete:
+ summary: Delete API token by ID
+ description: Delete an API token by their system-generated ID
+ operationId: deleteTokenById
tags:
- - Agent Operations
+ - API Tokens
security:
- - AgentAuth: []
+ - BasicAuth: []
responses:
- '200':
- description: Successful operation
+ '204':
+ description: API token deleted successfully
+ '400':
+ description: Bad request
content:
application/json:
schema:
- $ref: '#/components/schemas/Agent'
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Token not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/{agentId}:
+ /internal-accounts/{id}:
parameters:
- - name: agentId
+ - name: id
in: path
- description: System-generated unique agent identifier
+ description: The id of the internal account to update.
required: true
schema:
type: string
- get:
- summary: Get agent by ID
- description: Retrieve an agent by its system-generated ID.
- operationId: getAgentById
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ patch:
+ summary: Update internal account
+ description: |
+ Update mutable fields on an internal account. Today this supports updating the wallet privacy setting for an Embedded Wallet internal account.
+
+ Updating wallet privacy is a two-step signed-retry flow:
+
+ 1. Call `PATCH /internal-accounts/{id}` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same update fields submitted in step 1. The signed retry returns `200` with the updated internal account.
+ operationId: updateInternalAccount
tags:
- - Agent Management
+ - Internal Accounts
security:
- BasicAuth: []
+ parameters:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/InternalAccountUpdateRequest'
+ examples:
+ updateWalletPrivacy:
+ summary: Update wallet privacy request (both steps)
+ value:
+ privateEnabled: true
responses:
'200':
- description: Successful operation
+ description: Signed retry accepted. Returns the updated internal account.
content:
application/json:
schema:
- $ref: '#/components/schemas/Agent'
+ $ref: '#/components/schemas/InternalAccount'
+ examples:
+ enabled:
+ summary: Wallet privacy enabled
+ value:
+ id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ type: EMBEDDED_WALLET
+ status: ACTIVE
+ balance:
+ amount: 12550
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ totalBalance:
+ amount: 12550
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ fundingPaymentInstructions: []
+ privateEnabled: true
+ createdAt: '2026-04-08T15:30:00Z'
+ updatedAt: '2026-04-08T15:35:02Z'
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` (which binds the submitted update fields) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SignedRequestChallenge'
+ examples:
+ challenge:
+ summary: Internal account update challenge
+ value:
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"encoding":"PAYLOAD_ENCODING_HEXADECIMAL","hashFunction":"HASH_FUNCTION_NO_OP","payload":"9f3b...","signWith":"sp1q..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2"}'
+ requestId: Request:019542f5-b3e7-1d02-0000-000000000010
+ expiresAt: '2026-04-08T15:35:00Z'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending internal account update challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry body does not match the update fields bound into `payloadToSign` on the initial call.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent not found
+ description: Internal account not found
content:
application/json:
schema:
@@ -5798,27 +5312,69 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- patch:
- summary: Update agent
- description: Update an agent's name or paused state.
- operationId: updateAgent
+ /internal-accounts/{id}/export:
+ post:
+ summary: Export internal account wallet credentials
+ description: |
+ Export the wallet credentials of an Embedded Wallet internal account. The returned wallet credentials are HPKE-encrypted to the `clientPublicKey` supplied in the request body.
+
+ Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session):
+
+ 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key.
+
+ The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against.
+ operationId: exportInternalAccount
tags:
- - Agent Management
+ - Internal Accounts
security:
- BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the internal account to export.
+ required: true
+ schema:
+ type: string
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentUpdateRequest'
+ $ref: '#/components/schemas/InternalAccountExportRequest'
+ examples:
+ export:
+ summary: Export request (both steps)
+ value:
+ clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
responses:
'200':
- description: Agent updated successfully
+ description: Signed retry accepted. Returns the encrypted wallet credentials.
content:
application/json:
schema:
- $ref: '#/components/schemas/Agent'
+ $ref: '#/components/schemas/InternalAccountExportResponse'
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SignedRequestChallenge'
'400':
description: Bad request
content:
@@ -5826,13 +5382,13 @@ paths:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending export challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent not found
+ description: Internal account not found
content:
application/json:
schema:
@@ -5843,79 +5399,167 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- delete:
- summary: Delete agent
- description: Permanently delete an agent. Connected agent software will lose access immediately.
- operationId: deleteAgent
- tags:
- - Agent Management
- security:
- - BasicAuth: []
- responses:
- '204':
- description: Agent deleted successfully
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Agent not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /agents/{agentId}/policy:
- parameters:
- - name: agentId
- in: path
- description: System-generated unique agent identifier
- required: true
- schema:
- type: string
- patch:
- summary: Update agent policy
+ /auth/credentials:
+ post:
+ summary: Create an authentication credential
description: |
- Partially update an agent's policy. Only provided fields will be updated; omitted fields retain their current values. Policy changes take effect immediately.
- operationId: updateAgentPolicy
+ Register an authentication credential for an Embedded Wallet customer.
+
+ Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered.
+
+ Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.
+ operationId: createAuthCredential
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required on the signed retry.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering a credential; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentPolicyUpdateRequest'
+ $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf'
+ examples:
+ emailOtp:
+ summary: Add an email OTP credential
+ value:
+ type: EMAIL_OTP
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ smsOtp:
+ summary: Add an SMS OTP credential
+ value:
+ type: SMS_OTP
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ oauth:
+ summary: Add an OAuth credential
+ value:
+ type: OAUTH
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
+ passkey:
+ summary: Add a passkey credential
+ value:
+ type: PASSKEY
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ nickname: iPhone Face-ID
+ challenge: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx
+ attestation:
+ credentialId: AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY
+ clientDataJson: eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0
+ attestationObject: o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow
+ transports:
+ - internal
+ - hybrid
responses:
- '200':
- description: Agent policy updated successfully
+ '201':
+ description: Authentication credential created successfully. The body is the created `AuthMethod`. For `EMAIL_OTP`, the nickname is the customer email tied to the internal account; for `SMS_OTP`, it is the customer phone number. OTP responses that trigger a secure OTP challenge carry `otpEncryptionTargetBundle` — the HPKE target bundle the client uses to encrypt the OTP attempt on the subsequent `POST /auth/credentials/{id}/verify`. First-time EMAIL_OTP wallet bootstrap responses may omit that bundle; if it is absent, call `POST /auth/credentials/{id}/challenge` for the new credential to issue a fresh OTP and receive `otpEncryptionTargetBundle` before verifying. For `PASSKEY`, the credential must be authenticated for the first time via `POST /auth/credentials/{id}/challenge` followed by `POST /auth/credentials/{id}/verify` to produce a session — there is no inline authentication challenge on the registration response.
content:
application/json:
schema:
- $ref: '#/components/schemas/Agent'
+ $ref: '#/components/schemas/AuthMethodResponse'
+ examples:
+ emailOtp:
+ summary: Email OTP credential created
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: EMAIL_OTP
+ nickname: example@lightspark.com
+ otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ smsOtp:
+ summary: SMS OTP credential created
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: SMS_OTP
+ nickname: '+14155550123'
+ otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ oauth:
+ summary: OAuth credential created
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: OAUTH
+ nickname: example@lightspark.com
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ passkey:
+ summary: Passkey credential created
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: PASSKEY
+ nickname: iPhone Face-ID
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ '202':
+ description: Challenge issued. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthSignedRequestChallenge'
+ examples:
+ emailOtp:
+ summary: Additional email OTP credential challenge
+ value:
+ type: EMAIL_OTP
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userEmail":"jane@example.com","userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_EMAIL"}'
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
+ smsOtp:
+ summary: Additional SMS OTP credential challenge
+ value:
+ type: SMS_OTP
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F...","userPhoneNumber":"+14155550123"},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER"}'
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
+ oauth:
+ summary: Additional OAuth credential challenge
+ value:
+ type: OAUTH
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"oauthProviders":[{"oidcToken":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...","providerName":"Google"}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS"}'
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
+ passkey:
+ summary: Additional passkey credential challenge
+ value:
+ type: PASSKEY
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"authenticators":[{"attestation":{"attestationObject":"o2NmbXRk...","clientDataJson":"eyJjaGFsbGVuZ2UiOiJBcktRa...","credentialId":"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY"},"authenticatorName":"iPhone Face-ID","challenge":"ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx","transports":["internal","hybrid"]}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2"}'
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
'400':
- description: Bad request
+ description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `SMS_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an SMS OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer.
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent not found
+ description: Internal account not found
content:
application/json:
schema:
@@ -5926,32 +5570,62 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/{agentId}/device-codes:
- parameters:
- - name: agentId
- in: path
- description: System-generated unique agent identifier
- required: true
- schema:
- type: string
- post:
- summary: Regenerate a device code
- description: |
- Generate a new device code for an existing agent. Use this when the original device code has expired before being redeemed, or when the agent software needs to be reinstalled. Any previously issued unredeemed device codes for this agent are invalidated.
- operationId: regenerateAgentDeviceCode
+ get:
+ summary: List authentication credentials
+ description: |-
+ Retrieve all authentication credentials registered on an Embedded Wallet internal account.
+
+ The response is not paginated: an internal account is expected to have a small, bounded number of credentials (typically 1–5), so all results are returned inline. Additional per-credential detail (such as active session expiry) is available on `GET /auth/sessions`.
+ operationId: listAuthCredentials
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: accountId
+ in: query
+ description: Internal account id whose authentication credentials to list.
+ required: true
+ schema:
+ type: string
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
responses:
- '201':
- description: New device code generated successfully
+ '200':
+ description: Authentication credentials registered on the internal account. Returns an empty `data` array when the internal account has no credentials or when `accountId` does not match any internal account visible to the caller.
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentDeviceCode'
+ $ref: '#/components/schemas/AuthCredentialListResponse'
+ examples:
+ multipleCredentials:
+ summary: Internal account with multiple authentication credentials
+ value:
+ data:
+ - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: EMAIL_OTP
+ nickname: example@lightspark.com
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000004
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: OAUTH
+ nickname: example@lightspark.com
+ createdAt: '2026-04-08T15:35:00Z'
+ updatedAt: '2026-04-08T15:35:00Z'
+ - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000003
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: PASSKEY
+ credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
+ nickname: iPhone Face-ID
+ createdAt: '2026-04-09T10:15:00Z'
+ updatedAt: '2026-04-09T10:15:00Z'
+ empty:
+ summary: No credentials registered on the account
+ value:
+ data: []
'400':
- description: Bad request
+ description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier.
content:
application/json:
schema:
@@ -5962,332 +5636,352 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Agent not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Agent already has an active connection and cannot regenerate a device code
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/{agentId}/actions/{actionId}/approve:
- parameters:
- - name: agentId
- in: path
- description: System-generated unique agent identifier
- required: true
- schema:
- type: string
- - name: actionId
- in: path
- description: Unique identifier of the agent action to approve
- required: true
- schema:
- type: string
- post:
- summary: Approve an agent action
+ /auth/credentials/{id}:
+ delete:
+ summary: Revoke an authentication credential
description: |
- Approve a pending agent action, allowing Grid to proceed with execution. The action must have status `PENDING_APPROVAL`. Once approved, Grid executes the underlying operation (quote execution or transfer) and the action transitions to `APPROVED`.
- For `EXECUTE_QUOTE` actions, note that the underlying quote may have expired between submission and approval — in that case the action will transition to `FAILED` instead.
- This endpoint is called by the platform's backend using platform credentials, not by the agent itself.
- operationId: approveAgentAction
+ Revoke an authentication credential on an Embedded Wallet internal account.
+
+ Revocation is a two-step flow because it must be authorized by a session on a *different* credential on the same internal account:
+
+ 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`.
+
+ The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it.
+ operationId: revokeAuthCredential
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the authentication credential to revoke (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
+ required: true
+ schema:
+ type: string
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
responses:
- '200':
- description: Action approved successfully. Returns the updated AgentAction.
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry.
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/AuthSignedRequestChallenge'
+ '204':
+ description: Authentication credential revoked successfully.
'400':
- description: Bad request - Action cannot be approved
+ description: Bad request. Also returned when the target internal account has only a single authentication credential, which cannot be revoked via this endpoint.
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this credential, or when the `Request-Id` does not match an unexpired pending challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent or action not found
+ description: Authentication credential not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Action is not pending approval or has already been processed
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/{agentId}/actions/{actionId}/reject:
- parameters:
- - name: agentId
- in: path
- description: System-generated unique agent identifier
- required: true
- schema:
- type: string
- - name: actionId
- in: path
- description: Unique identifier of the agent action to reject
- required: true
- schema:
- type: string
+ /auth/credentials/{id}/verify:
post:
- summary: Reject an agent action
+ summary: Verify an authentication credential
description: |
- Reject a pending agent action, preventing execution. The action must have status `PENDING_APPROVAL`. Once rejected, the action transitions to `REJECTED` and the underlying operation is not executed.
- This endpoint is called by the platform's backend using platform credentials, not by the agent itself.
- operationId: rejectAgentAction
+ Complete the verification step for a previously created authentication credential and issue a session.
+
+ For `EMAIL_OTP` and `SMS_OTP` credentials, submit the `encryptedOtpBundle` produced by HPKE-encrypting `{otp_code, public_key}` under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued. The server is a pass-through and never sees the plaintext OTP code. On success the response is `202` with a `payloadToSign` carrying the `verificationToken` bound to the client's TEK public key — sign that token with the matching TEK private key, then retry the same request with the full stamp in `Grid-Wallet-Signature` and the `requestId` echoed in `Request-Id`. The signed retry returns `200` with the issued `AuthSession`. The TEK public key becomes the session API key on successful completion.
+ In sandbox mode, the OTP flow runs real HPKE end-to-end against a sandbox enclave keypair — clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code (`"000000"`) the user "receives" instead of a real email or SMS delivery.
+
+ For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request.
+
+ On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires.
+ operationId: verifyAuthCredential
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
+ required: true
+ schema:
+ type: string
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the TEK (Target Encryption Key) keypair the client generated for this login. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification. Not used by `OAUTH` or `PASSKEY` verification, which complete in a single call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response from this endpoint, echoed back exactly here so the server can correlate the signed retry with the issued challenge. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification; must be paired with `Grid-Wallet-Signature`. For `PASSKEY` verification, the `requestId` issued from `POST /auth/credentials/{id}/challenge` is echoed here instead so the server can correlate the assertion with the pending challenge.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
- required: false
+ required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentActionRejectRequest'
+ $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf'
+ examples:
+ emailOtp:
+ summary: Verify an email OTP credential (first leg)
+ value:
+ type: EMAIL_OTP
+ encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
+ smsOtp:
+ summary: Verify an SMS OTP credential (first leg)
+ value:
+ type: SMS_OTP
+ encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
+ emailOtpSignedRetry:
+ summary: Signed retry completing an email OTP verification
+ description: Same request body as the first leg, plus the `Grid-Wallet-Signature` and `Request-Id` headers carrying the stamp over the `verificationToken` and the `requestId` from the prior `202` response.
+ value:
+ type: EMAIL_OTP
+ encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
+ oauth:
+ summary: Verify an OAuth credential
+ value:
+ type: OAUTH
+ oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
+ clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ passkey:
+ summary: Verify a passkey credential
+ value:
+ type: PASSKEY
+ assertion:
+ credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
+ clientDataJson: eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0
+ authenticatorData: PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA
+ signature: MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6jhd45bDx92wjXKs900
responses:
'200':
- description: Action rejected successfully. Returns the updated AgentAction.
+ description: Authentication credential verified and session issued
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/AuthSession'
+ '202':
+ description: Verification challenge issued. Returned only for OTP credentials, on the first leg of the secure OTP login flow. Build an API-key stamp over `payloadToSign` (the `verificationToken`) with the TEK keypair the client generated for this login, then resubmit the same request with that full stamp as `Grid-Wallet-Signature` and `requestId` echoed as `Request-Id` to receive the issued session on the signed retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthSignedRequestChallenge'
+ examples:
+ emailOtp:
+ summary: Email OTP verification challenge (sign and retry)
+ value:
+ type: EMAIL_OTP
+ payloadToSign: eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ.eyJzdWIiOiJUWnk2NkVPa1RGYTd2NkpXZ0VxaVgyZGFXOENXc2pMQzVDVU9aRUlGY3hzIiwiaWF0IjoxNzc5NDA3MjIxLCJleHAiOjE3Nzk0MTA4MjF9.gKX9MWYGkw8Y55bgzsgrRftvUHFruIe8yu0w9Kpjp5qnrZnXcTV71WVoltGPsr015IY_oRTOkIFLHmiGNG9zBw
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
'400':
- description: Bad request - Action cannot be rejected
+ description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP` or `SMS_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. For OTP signed retries, returned when `Grid-Wallet-Signature` is missing, malformed, signed by a public key that does not match the one bound into the `verificationToken`, or when `Request-Id` does not match an unexpired pending verification challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent or action not found
+ description: Authentication credential not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Action is not pending approval or has already been processed
+ '429':
+ description: Too many requests. Returned with `RATE_LIMITED` when verification attempts for this credential happen too frequently (for example, repeated bad OTPs or rapid-fire reauthentication retries). Clients should back off and retry after the interval indicated by the `Retry-After` response header.
+ headers:
+ Retry-After:
+ description: Number of seconds to wait before retrying the request.
+ schema:
+ type: integer
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/device-codes/{code}/status:
- parameters:
- - name: code
- in: path
- description: The device code to check
- required: true
- schema:
- type: string
- get:
- summary: Get device code status
+ /auth/credentials/{id}/challenge:
+ post:
+ summary: Re-issue an authentication credential challenge
description: |
- Check whether a device code has been redeemed. Use this to poll for agent installation completion after creating an agent.
- operationId: getAgentDeviceCodeStatus
+ Re-issue the challenge for an existing authentication credential.
+
+ For `EMAIL_OTP` and `SMS_OTP` credentials, this triggers a new one-time password to the contact on file and returns a fresh `otpEncryptionTargetBundle` for the client to HPKE-encrypt the OTP attempt against. After the user receives the new OTP, build the `encryptedOtpBundle` under the new target bundle and call `POST /auth/credentials/{id}/verify` to begin the secure OTP login flow.
+
+ `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`.
+
+ For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session.
+ operationId: challengeAuthCredential
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the authentication credential to re-challenge (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
+ required: true
+ schema:
+ type: string
+ requestBody:
+ description: Request body. Required when re-challenging a `PASSKEY` credential (must carry `clientPublicKey`). Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient — the OTP is delivered out-of-band. OAuth credentials do not use this endpoint.
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthCredentialChallengeRequest'
+ examples:
+ passkey:
+ summary: Re-challenge a passkey credential
+ value:
+ clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ emailOtp:
+ summary: Re-challenge an email-OTP credential (empty body)
+ value: {}
+ smsOtp:
+ summary: Re-challenge an SMS-OTP credential (empty body)
+ value: {}
responses:
'200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AgentDeviceCodeStatusResponse'
- '401':
- description: Unauthorized
+ description: Challenge re-issued for the authentication credential. For `EMAIL_OTP` and `SMS_OTP` the body is a plain `AuthMethod` and a new OTP has been sent. For `PASSKEY` the body is a `PasskeyAuthChallenge` carrying the passkey `credentialId`, freshly issued `challenge`, `requestId`, and `expiresAt` required to complete reauthentication via `POST /auth/credentials/{id}/verify`.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Device code not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
+ $ref: '#/components/schemas/AuthCredentialResponseOneOf'
+ examples:
+ emailOtp:
+ summary: Email OTP challenge re-issued
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: EMAIL_OTP
+ nickname: example@lightspark.com
+ otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:35:00Z'
+ passkey:
+ summary: Passkey reauthentication challenge issued
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: PASSKEY
+ credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
+ nickname: iPhone Face-ID
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:35:00Z'
+ challenge: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
+ '400':
+ description: Bad request
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- /agents/device-codes/{code}/redeem:
- parameters:
- - name: code
- in: path
- description: The device code to redeem
- required: true
- schema:
- type: string
- post:
- summary: Redeem device code
- description: |
- Redeem a device code to obtain agent credentials. This endpoint is called by the agent software during installation. On success, returns a Bearer access token that the agent uses for all subsequent API calls. The token is returned only once and must be stored securely.
- This endpoint does not require platform authentication — the device code itself serves as proof of authorization.
- operationId: redeemAgentDeviceCode
- tags:
- - Agent Management
- security: []
- responses:
- '200':
- description: Device code redeemed successfully
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse'
- '400':
- description: Bad request (e.g., code already redeemed or expired)
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Authentication credential not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
- '404':
- description: Device code not found
+ $ref: '#/components/schemas/Error404'
+ '429':
+ description: Too many requests. Returned with `RATE_LIMITED` when challenge re-issues are requested more frequently than the credential challenge rate limit allows. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
+ headers:
+ Retry-After:
+ description: Number of seconds to wait before retrying the request.
+ schema:
+ type: integer
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/transactions:
+ /auth/sessions:
get:
- summary: List agent transactions
- description: |
- Retrieve a paginated list of transactions for the authenticated agent's customer. Results are automatically scoped to the agent's associated customer — no customer filter is needed or accepted.
- operationId: agentListTransactions
+ summary: List active sessions
+ description: |-
+ Retrieve all active authentication sessions on an Embedded Wallet internal account. A session is created each time a credential is verified via `POST /auth/credentials/{id}/verify`, and remains active until its `expiresAt` passes or it is revoked via `DELETE /auth/sessions/{id}`.
+
+ The response is not paginated: an internal account is expected to have a small, bounded number of concurrent sessions (one per signed-in device, typically 1–4), so all results are returned inline.
+ operationId: listAuthSessions
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: accountIdentifier
- in: query
- description: Filter by account identifier (matches either sender or receiver)
- required: false
- schema:
- type: string
- - name: senderAccountIdentifier
- in: query
- description: Filter by sender account identifier
- required: false
- schema:
- type: string
- - name: receiverAccountIdentifier
- in: query
- description: Filter by receiver account identifier
- required: false
- schema:
- type: string
- - name: status
- in: query
- description: Filter by transaction status
- required: false
- schema:
- $ref: '#/components/schemas/TransactionStatus'
- - name: type
- in: query
- description: Filter by transaction type
- required: false
- schema:
- $ref: '#/components/schemas/TransactionType'
- - name: reference
- in: query
- description: Filter by reference
- required: false
- schema:
- type: string
- - name: startDate
- in: query
- description: Filter by start date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: endDate
- in: query
- description: Filter by end date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
- - name: sortOrder
+ - name: accountId
in: query
- description: Order to sort results in
- required: false
+ description: Internal account id whose sessions to list.
+ required: true
schema:
type: string
- enum:
- - asc
- - desc
- default: desc
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
responses:
'200':
- description: Successful operation
+ description: Active authentication sessions on the internal account. Returns an empty `data` array when the internal account has no active sessions or when `accountId` does not match any internal account visible to the caller.
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionListResponse'
+ $ref: '#/components/schemas/SessionListResponse'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier.
content:
application/json:
schema:
@@ -6304,38 +5998,66 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/transactions/{transactionId}:
- parameters:
- - name: transactionId
- in: path
- description: Unique identifier of the transaction
- required: true
- schema:
- type: string
- get:
- summary: Get agent transaction by ID
+ /auth/sessions/{id}:
+ delete:
+ summary: Revoke an authentication session
description: |
- Retrieve a specific transaction belonging to the authenticated agent's customer. Returns 404 if the transaction exists but belongs to a different customer.
- operationId: agentGetTransaction
+ Revoke an authentication session on an Embedded Wallet internal account. Revocation is a two-step signed-retry flow:
+
+ 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`.
+
+ Sessions also expire on their own. `404` is returned whenever the `id` does not match an active session — whether the session was never issued, was already revoked by a prior call, or has expired past its `expiresAt`. The response code reflects the resource state, not an error in the client's flow: re-revoking an already-revoked or expired session is safe and idempotent at the user intent level.
+ operationId: revokeAuthSession
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the session to revoke.
+ required: true
+ schema:
+ type: string
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
responses:
- '200':
- description: Successful operation
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry.
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionOneOf'
+ $ref: '#/components/schemas/AuthSignedRequestChallenge'
+ '204':
+ description: Session revoked successfully.
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this session, or when the `Request-Id` does not match an unexpired pending challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Transaction not found
+ description: Session not found
content:
application/json:
schema:
@@ -6346,107 +6068,101 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/quotes:
+ /auth/sessions/{id}/refresh:
post:
- summary: Create a transfer quote
+ summary: Refresh an authentication session
description: |
- Generate a quote for a cross-currency transfer on behalf of the authenticated agent's customer. Accounts referenced in the request must belong to the agent's customer. Requires the CREATE_QUOTES permission in the agent's policy.
- If the agent's defaultExecutionMode is APPROVAL_REQUIRED, or the quote amount exceeds the agent's approvalThresholds, the resulting transaction will require explicit approval before funds move.
- operationId: agentCreateQuote
+ Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow:
+
+ 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key.
+
+ The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead.
+ operationId: refreshAuthSession
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: Idempotency-Key
+ - name: id
+ in: path
+ description: The id of the active session to refresh.
+ required: true
+ schema:
+ type: string
+ example: Session:019542f5-b3e7-1d02-0000-000000000003
+ - name: Grid-Wallet-Signature
in: header
required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ description: Full API-key stamp built over the prior `payloadToSign` with the current session API keypair. Required on the signed retry; ignored on the initial call.
schema:
type: string
- example:
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in the prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:019542f5-b3e7-1d02-0000-000000000010
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/QuoteRequest'
+ $ref: '#/components/schemas/AuthSessionRefreshRequest'
+ examples:
+ refresh:
+ summary: Refresh an active session
+ value:
+ clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
responses:
'201':
- description: Transfer quote created successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Quote'
- '400':
- description: Bad request - Missing or invalid parameters
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Agent policy does not permit this operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
- '412':
- description: Counterparty doesn't support UMA version
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error412'
- '424':
- description: Counterparty issue
+ description: New authentication session created successfully.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error424'
- '500':
- description: Internal service error
+ $ref: '#/components/schemas/AuthSession'
+ examples:
+ session:
+ summary: Refreshed authentication session
+ value:
+ id: Session:019542f5-b3e7-1d02-0000-000000000011
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: EMAIL_OTP
+ encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
+ nickname: example@lightspark.com
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:35:00Z'
+ expiresAt: '2026-04-08T15:50:00Z'
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the current session API keypair, then echo `requestId` on the signed retry.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- /agents/me/quotes/{quoteId}:
- parameters:
- - name: quoteId
- in: path
- description: ID of the quote to retrieve
- required: true
- schema:
- type: string
- get:
- summary: Get agent quote by ID
- description: |
- Retrieve a quote created by the authenticated agent. Returns 404 if the quote exists but was not created by this agent.
- operationId: agentGetQuote
- tags:
- - Agent Operations
- security:
- - AgentAuth: []
- responses:
- '200':
- description: Quote retrieved successfully
+ $ref: '#/components/schemas/SignedRequestChallenge'
+ examples:
+ challenge:
+ summary: Session refresh challenge
+ value:
+ payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}'
+ requestId: Request:019542f5-b3e7-1d02-0000-000000000010
+ expiresAt: '2026-04-08T15:35:00Z'
+ '400':
+ description: Bad request
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the `BasicAuth` credentials are missing or invalid, when the target session is no longer active and cannot be used for refresh, when the signed retry omits `Grid-Wallet-Signature`, when the provided signature is malformed or does not match the pending refresh challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Quote not found
+ description: Session not found
content:
application/json:
schema:
@@ -6457,75 +6173,100 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/quotes/{quoteId}/execute:
- parameters:
- - name: quoteId
- in: path
- required: true
- description: The unique identifier of the quote to execute
- schema:
- type: string
- example: Quote:019542f5-b3e7-1d02-0000-000000000001
+ /auth/delegated-keys:
post:
- summary: Execute a quote
+ summary: Create a delegated signing key
description: |
- Execute a quote created by the authenticated agent. Requires the EXECUTE_QUOTES permission in the agent's policy.
- If the agent's policy requires approval for this amount (based on execution mode or approval thresholds), the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
- Once executed, the quote cannot be cancelled.
- operationId: agentExecuteQuote
+ Delegate Spark token-transaction signing authority for a card funding source backed by an Embedded Wallet internal account to a Grid-custodied P-256 API key. Grid uses the requested card and internal account to identify the wallet funding source, generates the keypair server-side, creates an isolated signer identity holding the public key, then policies granting that identity signing and self-revocation authority. The private key is custodied by Grid and never returned. Both activities must be authorized by the wallet owner, so creation is a three-leg signed-retry flow:
+
+ 1. Call `POST /auth/delegated-keys` with no signature headers. Grid generates the delegated keypair and the response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of a verified credential on the requested Embedded Wallet internal account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The response is a second `202` with a new `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 3. Stamp the new `payloadToSign` with the same session keypair and retry once more with the new `Request-Id`. The signed retry returns `201` with the created `DelegatedKey` in `ACTIVE` status.
+
+ The same request body must be sent on all three legs. A flow abandoned after the second leg leaves the key in `PENDING` status: the signer identity exists but holds no policies, so it cannot sign or revoke itself. Abandoned `PENDING` keys do not block creating another delegated key. After activation, Grid uses the custodied key to authorize signing for the card's Embedded Wallet funding account in place of a session keypair; the platform never handles the key material.
+
+ Each card funding source may have at most one `ACTIVE` delegated key for its Embedded Wallet funding account; revoke the existing active key before creating a new one. A delegated key authorizes raw-payload signing for the wallet and cannot be scoped to amounts or recipients by the public API. Revoke it with `DELETE /auth/delegated-keys/{id}` when no longer needed.
+ operationId: createDelegatedKey
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: Idempotency-Key
+ - name: Grid-Wallet-Signature
in: header
required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified credential on the same internal account. Required on the signed retries; ignored on the initial call.
schema:
type: string
- example:
- - name: Grid-Wallet-Signature
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
in: header
required: false
- description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ description: The `requestId` returned in the prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retries; must be paired with `Grid-Wallet-Signature`.
schema:
type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DelegatedKeyCreateRequest'
+ examples:
+ create:
+ summary: Delegate signing to a Grid-custodied key
+ value:
+ cardId: Card:019542f5-b3e7-1d02-0000-000000000010
+ internalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ nickname: Card payments key
responses:
- '200':
- description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.'
+ '201':
+ description: Delegated key created and policy granted. The key is `ACTIVE` and Grid may use it to stamp card-payment quote executions for this card funding source's Embedded Wallet funding account.
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/DelegatedKey'
+ examples:
+ delegatedKey:
+ summary: Active delegated key
+ value:
+ id: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
+ cardId: Card:019542f5-b3e7-1d02-0000-000000000010
+ fundingSourceId: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ publicKey: 02a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90
+ nickname: Card payments key
+ status: ACTIVE
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:42Z'
+ '202':
+ description: Challenge issued for the next leg. Stamp `payloadToSign` and retry the same request with `Grid-Wallet-Signature` and `Request-Id`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DelegatedKeySignedRequestChallenge'
'400':
- description: Bad request - Quote cannot be executed
+ description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`.
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing on a retry, malformed, or does not match the pending challenge, or when the `Request-Id` does not match an unexpired pending challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Quote not found
+ description: Card, card funding source, or Embedded Wallet funding account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
'409':
- description: Conflict - Quote already executed, expired, or in invalid state
+ description: An `ACTIVE` delegated key already exists for this card funding source. Revoke it with `DELETE /auth/delegated-keys/{id}` before creating a new one.
content:
application/json:
schema:
@@ -6536,81 +6277,78 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/actions:
get:
- summary: List agent's own actions
- description: |
- Retrieve a paginated list of actions submitted by the authenticated agent. Use this to poll for approval decisions after submitting an action that requires approval.
- operationId: agentListActions
+ summary: List delegated signing keys
+ description: List delegated signing keys for an Embedded Wallet internal account, a card funding source, or both, including `PENDING` keys (user created but policy leg never completed) and `REVOKED` keys. At least one of `accountId` or `fundingSourceId` must be supplied.
+ operationId: listDelegatedKeys
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: status
- in: query
- description: Filter by action status
- required: false
- schema:
- $ref: '#/components/schemas/AgentActionStatus'
- - name: limit
+ - name: accountId
in: query
- description: Maximum number of results to return (default 20, max 100)
required: false
+ description: The id of the internal account whose delegated keys to list.
schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
+ type: string
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - name: fundingSourceId
in: query
- description: Cursor for pagination (returned from previous request)
required: false
+ description: The id of the card funding source whose delegated keys to list.
schema:
type: string
+ example: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011
responses:
'200':
- description: Successful operation
+ description: Delegated keys matching the supplied filters. Returns an empty `data` array when no matching delegated keys are visible to the caller.
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentActionListResponse'
- '401':
- description: Unauthorized
+ $ref: '#/components/schemas/DelegatedKeyListResponse'
+ '400':
+ description: Bad request
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/actions/{actionId}:
- parameters:
- - name: actionId
- in: path
- description: Unique identifier of the agent action
- required: true
- schema:
- type: string
+ /auth/delegated-keys/{id}:
get:
- summary: Get an agent action
- description: |
- Retrieve a specific action submitted by the authenticated agent. Poll this endpoint after submitting an action that requires approval to check whether it has been approved, rejected, or has failed.
- operationId: agentGetAction
+ summary: Get a delegated signing key
+ description: Retrieve a delegated signing key by its system-generated id.
+ operationId: getDelegatedKeyById
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the delegated key to retrieve (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys` or `GET /auth/delegated-keys`).
+ required: true
+ schema:
+ type: string
+ example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/DelegatedKey'
'401':
description: Unauthorized
content:
@@ -6618,7 +6356,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Action not found
+ description: Delegated key not found
content:
application/json:
schema:
@@ -6629,69 +6367,42 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/transfer-in:
- post:
- summary: Create a transfer-in
+ delete:
+ summary: Revoke a delegated signing key
description: |
- Transfer funds from an external account to an internal account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy.
- If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
- This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the payment instructions on the internal account to deposit funds.
- operationId: agentCreateTransferIn
+ Revoke an `ACTIVE` delegated signing key. Grid uses the custodied delegated key to authorize deleting its own signer identity. Deleting the identity also removes its API key, after which the delegated key can no longer sign. The response is `204` when revocation completes.
+
+ The underlying signing policies are left in place. Their consensus references the now-deleted signer identity, so they can never authorize anything, and deleting them is unnecessary for correctness or security.
+ operationId: revokeDelegatedKey
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ - name: id
+ in: path
+ description: The id of the delegated key to revoke (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys`).
+ required: true
schema:
type: string
- example: 550e8400-e29b-41d4-a716-446655440000
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TransferInRequest'
- examples:
- transferIn:
- summary: Transfer from external to internal account
- value:
- source:
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- amount: 12550
+ example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
responses:
- '201':
- description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AgentAction'
+ '204':
+ description: Delegated key revoked. The key can no longer authorize signing.
'400':
- description: Bad request - Invalid parameters
+ description: Bad request. Returned when the delegated key has already been revoked or is not `ACTIVE`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Account not found
+ description: Delegated key not found
content:
application/json:
schema:
@@ -6702,50 +6413,31 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/transfer-out:
+ /agents:
post:
- summary: Create a transfer-out
+ summary: Create an agent
description: |
- Transfer funds from an internal account to an external account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy.
- If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
- operationId: agentCreateTransferOut
+ Create a new agent with a specified policy. Returns the created agent and a device code that must be redeemed by the agent software to complete installation.
+ operationId: createAgent
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
- parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
- schema:
- type: string
- example: 550e8400-e29b-41d4-a716-446655440000
+ - BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/TransferOutRequest'
- examples:
- transferOut:
- summary: Transfer from internal to external account
- value:
- source:
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- destination:
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- amount: 12550
+ $ref: '#/components/schemas/AgentCreateRequest'
responses:
'201':
- description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated.
+ description: Agent created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/AgentCreateResponse'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request
content:
application/json:
schema:
@@ -6756,47 +6448,67 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
- '404':
- description: Account not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/internal-accounts:
get:
- summary: List agent's internal accounts
- description: |
- Retrieve the internal accounts belonging to the customer this agent operates on behalf of. Use this to discover available source accounts for transfers and quotes, and to verify which accounts are accessible under the agent's `accountRestrictions` policy.
- operationId: agentListInternalAccounts
+ summary: List agents
+ description: Retrieve a paginated list of agents for the authenticated platform.
+ operationId: listAgents
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: currency
+ - name: customerId
in: query
- description: Filter by currency code
+ description: Filter by customer ID
required: false
schema:
type: string
- - name: type
+ - name: isPaused
in: query
- description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for the customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for platform-managed holding accounts.
+ description: Filter by paused status
required: false
schema:
- $ref: '#/components/schemas/InternalAccountType'
+ type: boolean
+ - name: isConnected
+ in: query
+ description: Filter by connection status (whether the device code has been redeemed)
+ required: false
+ schema:
+ type: boolean
+ - name: createdAfter
+ in: query
+ description: Filter agents created after this timestamp (inclusive)
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: createdBefore
+ in: query
+ description: Filter agents created before this timestamp (inclusive)
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: updatedAfter
+ in: query
+ description: Filter agents updated after this timestamp (inclusive)
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: updatedBefore
+ in: query
+ description: Filter agents updated before this timestamp (inclusive)
+ required: false
+ schema:
+ type: string
+ format: date-time
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -6818,7 +6530,13 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/InternalAccountListResponse'
+ $ref: '#/components/schemas/AgentListResponse'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -6831,23 +6549,43 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/external-accounts:
+ /agents/approvals:
get:
- summary: List agent external accounts
+ summary: List agent transaction approval requests
description: |
- Retrieve a paginated list of external accounts belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
- operationId: agentListExternalAccounts
+ Retrieve a paginated list of agent actions that require platform approval. Filter by `agentId` or `customerId` to scope results to a specific agent or customer. Approve or reject individual actions via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`.
+ operationId: listAgentApprovals
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: currency
+ - name: agentId
in: query
- description: Filter by currency code
+ description: Filter by agent ID
+ required: false
+ schema:
+ type: string
+ - name: customerId
+ in: query
+ description: Filter by customer ID
+ required: false
+ schema:
+ type: string
+ - name: startDate
+ in: query
+ description: Filter by start date (inclusive) in ISO 8601 format
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: endDate
+ in: query
+ description: Filter by end date (inclusive) in ISO 8601 format
required: false
schema:
type: string
+ format: date-time
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -6863,13 +6601,23 @@ paths:
required: false
schema:
type: string
- responses:
- '200':
- description: Successful operation
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
+ responses:
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccountListResponse'
+ $ref: '#/components/schemas/AgentActionListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -6888,105 +6636,58 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- post:
- summary: Add an external account
+ /agents/me:
+ get:
+ summary: Get current agent
description: |
- Register a new external bank account or wallet for the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. The `customerId` field is optional and will be inferred from the agent's associated customer if omitted.
- operationId: agentCreateExternalAccount
+ Retrieve the authenticated agent's own profile, policy, and current usage. This endpoint is called by the agent software itself using its own credentials (obtained via device code redemption) rather than platform credentials.
+ operationId: getAgentMe
tags:
- Agent Operations
security:
- AgentAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ExternalAccountCreateRequest'
- examples:
- usBankAccount:
- summary: Create external US bank account
- value:
- currency: USD
- accountInfo:
- accountType: USD_ACCOUNT
- accountNumber: '12345678901'
- routingNumber: '123456789'
- bankAccountType: CHECKING
- bankName: Chase Bank
- beneficiary:
- beneficiaryType: INDIVIDUAL
- fullName: John Doe
- birthDate: '1990-01-15'
- nationality: US
- address:
- line1: 123 Main Street
- city: San Francisco
- state: CA
- postalCode: '94105'
- country: US
- sparkWallet:
- summary: Create external Spark wallet
- value:
- currency: BTC
- accountInfo:
- accountType: SPARK_WALLET
- address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
responses:
- '201':
- description: External account created successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ExternalAccount'
- '400':
- description: Bad request
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/Agent'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - External account already exists
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/external-accounts/{externalAccountId}:
+ /agents/{agentId}:
parameters:
- - name: externalAccountId
+ - name: agentId
in: path
- description: System-generated unique external account identifier
+ description: System-generated unique agent identifier
required: true
schema:
type: string
get:
- summary: Get agent external account by ID
- description: |
- Retrieve an external account belonging to the authenticated agent's customer. Returns 404 if the account exists but belongs to a different customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
- operationId: agentGetExternalAccount
+ summary: Get agent by ID
+ description: Retrieve an agent by its system-generated ID.
+ operationId: getAgentById
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
+ - BasicAuth: []
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccount'
+ $ref: '#/components/schemas/Agent'
'401':
description: Unauthorized
content:
@@ -6994,7 +6695,52 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: External account not found
+ description: Agent not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ patch:
+ summary: Update agent
+ description: Update an agent's name or paused state.
+ operationId: updateAgent
+ tags:
+ - Agent Management
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentUpdateRequest'
+ responses:
+ '200':
+ description: Agent updated successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Agent'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Agent not found
content:
application/json:
schema:
@@ -7006,17 +6752,16 @@ paths:
schema:
$ref: '#/components/schemas/Error500'
delete:
- summary: Delete agent external account
- description: |
- Delete an external account belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
- operationId: agentDeleteExternalAccount
+ summary: Delete agent
+ description: Permanently delete an agent. Connected agent software will lose access immediately.
+ operationId: deleteAgent
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
+ - BasicAuth: []
responses:
'204':
- description: External account deleted successfully
+ description: Agent deleted successfully
'401':
description: Unauthorized
content:
@@ -7024,7 +6769,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: External account not found
+ description: Agent not found
content:
application/json:
schema:
@@ -7035,18 +6780,21 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /cards:
- post:
- summary: Issue a card
+ /agents/{agentId}/policy:
+ parameters:
+ - name: agentId
+ in: path
+ description: System-generated unique agent identifier
+ required: true
+ schema:
+ type: string
+ patch:
+ summary: Update agent policy
description: |
- Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`.
-
- If any funding source is an Embedded Wallet internal account, the cardholder must authorize Grid to sign Spark token transactions for that card funding source by completing the delegated-key creation flow with `POST /auth/delegated-keys`. Until an active delegated key exists for that funding source, Authorization Decisioning cannot use it to fund card transactions.
-
- New cards start in `state: "PROCESSING"` while the card issuer provisions the card. The `card.state_change` webhook fires on each state transition, including the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails).
- operationId: createCard
+ Partially update an agent's policy. Only provided fields will be updated; omitted fields retain their current values. Policy changes take effect immediately.
+ operationId: updateAgentPolicy
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
requestBody:
@@ -7054,25 +6802,16 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/CardCreateRequest'
- examples:
- virtualCard:
- summary: Issue a virtual card with one funding source
- value:
- cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCardId: card-emp-aary-001
- form: VIRTUAL
- fundingSources:
- - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ $ref: '#/components/schemas/AgentPolicyUpdateRequest'
responses:
- '201':
- description: Card created successfully. Newly-created cards start in `PROCESSING` while the issuer provisions them. Cards funded by an Embedded Wallet internal account also require an active delegated key for that funding source before Authorization Decisioning can use it.
+ '200':
+ description: Agent policy updated successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Card'
+ $ref: '#/components/schemas/Agent'
'400':
- description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters.
+ description: Bad request
content:
application/json:
schema:
@@ -7083,86 +6822,44 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
+ '404':
+ description: Agent not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Card issuance is not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error501'
- get:
- summary: List cards
+ $ref: '#/components/schemas/Error500'
+ /agents/{agentId}/device-codes:
+ parameters:
+ - name: agentId
+ in: path
+ description: System-generated unique agent identifier
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Regenerate a device code
description: |
- Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller.
- operationId: listCards
+ Generate a new device code for an existing agent. Use this when the original device code has expired before being redeemed, or when the agent software needs to be reinstalled. Any previously issued unredeemed device codes for this agent are invalidated.
+ operationId: regenerateAgentDeviceCode
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
- parameters:
- - name: cardholderId
- in: query
- description: Filter by cardholder (customer) id.
- required: false
- schema:
- type: string
- - name: accountId
- in: query
- description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id.
- required: false
- schema:
- type: string
- - name: platformCardId
- in: query
- description: Filter by platform-specific card identifier.
- required: false
- schema:
- type: string
- - name: state
- in: query
- description: Filter by card state.
- required: false
- schema:
- $ref: '#/components/schemas/CardState'
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
- - name: sortOrder
- in: query
- description: Order to sort results in
- required: false
- schema:
- type: string
- enum:
- - asc
- - desc
- default: desc
responses:
- '200':
- description: Successful operation
+ '201':
+ description: New device code generated successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/CardListResponse'
+ $ref: '#/components/schemas/AgentDeviceCode'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request
content:
application/json:
schema:
@@ -7173,41 +6870,62 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
+ '404':
+ description: Agent not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: Conflict - Agent already has an active connection and cannot regenerate a device code
content:
application/json:
schema:
- $ref: '#/components/schemas/Error501'
- /cards/{id}:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /agents/{agentId}/actions/{actionId}/approve:
parameters:
- - name: id
+ - name: agentId
in: path
- description: System-generated unique card identifier
+ description: System-generated unique agent identifier
required: true
schema:
type: string
- get:
- summary: Get a card
- description: Retrieve a card by its system-generated id. To display the card's full PAN, CVV, and expiry to the cardholder, request a reveal with `POST /cards/{id}/reveal` — the card resource itself never carries the reveal URL.
- operationId: getCardById
+ - name: actionId
+ in: path
+ description: Unique identifier of the agent action to approve
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Approve an agent action
+ description: |
+ Approve a pending agent action, allowing Grid to proceed with execution. The action must have status `PENDING_APPROVAL`. Once approved, Grid executes the underlying operation (quote execution or transfer) and the action transitions to `APPROVED`.
+ For `EXECUTE_QUOTE` actions, note that the underlying quote may have expired between submission and approval — in that case the action will transition to `FAILED` instead.
+ This endpoint is called by the platform's backend using platform credentials, not by the agent itself.
+ operationId: approveAgentAction
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
responses:
'200':
- description: Successful operation
+ description: Action approved successfully. Returns the updated AgentAction.
content:
application/json:
schema:
- $ref: '#/components/schemas/Card'
+ $ref: '#/components/schemas/AgentAction'
+ '400':
+ description: Bad request - Action cannot be approved
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -7215,128 +6933,80 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Card not found
+ description: Agent or action not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
+ '409':
+ description: Conflict - Action is not pending approval or has already been processed
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error501'
- patch:
- summary: Update a card
+ $ref: '#/components/schemas/Error500'
+ /agents/{agentId}/actions/{actionId}/reject:
+ parameters:
+ - name: agentId
+ in: path
+ description: System-generated unique agent identifier
+ required: true
+ schema:
+ type: string
+ - name: actionId
+ in: path
+ description: Unique identifier of the agent action to reject
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Reject an agent action
description: |
- Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied.
-
- - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`.
- - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`.
-
- Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`):
-
- 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
-
- 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`.
-
- Effects:
- - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed.
- - `state: ACTIVE`: normal authorization behavior resumes.
- - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys.
- - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding.
-
- The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated.
- operationId: updateCardById
+ Reject a pending agent action, preventing execution. The action must have status `PENDING_APPROVAL`. Once rejected, the action transitions to `REJECTED` and the underlying operation is not executed.
+ This endpoint is called by the platform's backend using platform credentials, not by the agent itself.
+ operationId: rejectAgentAction
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
- parameters:
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/CardUpdateRequest'
- examples:
- freeze:
- summary: Freeze an active card
- value:
- state: FROZEN
- unfreeze:
- summary: Unfreeze a frozen card
- value:
- state: ACTIVE
- updateFundingSources:
- summary: Replace the card's bound funding sources
- value:
- fundingSources:
- - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
- freezeAndUpdateSources:
- summary: Freeze the card and replace its funding sources in one call
- value:
- state: FROZEN
- fundingSources:
- - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- close:
- summary: Permanently close the card
- value:
- state: CLOSED
+ $ref: '#/components/schemas/AgentActionRejectRequest'
responses:
'200':
- description: Signed retry accepted. Returns the updated card.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Card'
- '202':
- description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry.
+ description: Action rejected successfully. Returns the updated AgentAction.
content:
application/json:
schema:
- $ref: '#/components/schemas/SignedRequestChallenge'
+ $ref: '#/components/schemas/AgentAction'
'400':
- description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters.
+ description: Bad request - Action cannot be rejected
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Card not found
+ description: Agent or action not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
'409':
- description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.'
+ description: Conflict - Action is not pending approval or has already been processed
content:
application/json:
schema:
@@ -7347,54 +7017,38 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error501'
- /cards/{id}/reveal:
+ /agents/device-codes/{code}/status:
parameters:
- - name: id
+ - name: code
in: path
- description: System-generated unique card identifier
+ description: The device code to check
required: true
schema:
type: string
- post:
- summary: Reveal card details
- description: |-
- Mint a signed, short-lived URL for the card processor's iframe that displays the card's full PAN, CVV, and expiry to the cardholder. This is the only way to obtain a reveal URL — the `Card` resource never carries one.
-
- Request the reveal right before rendering the iframe and render the returned `panEmbedUrl` immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log the URL — it is a bearer secret for the full card details. The card data renders inside the processor's iframe and never crosses Grid's or your servers.
-
- Every reveal is audit-logged with the requesting actor.
- operationId: revealCard
+ get:
+ summary: Get device code status
+ description: |
+ Check whether a device code has been redeemed. Use this to poll for agent installation completion after creating an agent.
+ operationId: getAgentDeviceCodeStatus
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
responses:
'200':
- description: Reveal URL minted.
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/CardRevealResponse'
+ $ref: '#/components/schemas/AgentDeviceCodeStatusResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden. The session has no attributable actor to audit the reveal against (for example, an impersonated dashboard session).
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Card not found
+ description: Device code not found
content:
application/json:
schema:
@@ -7405,90 +7059,38 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error501'
- /sandbox/cards/{id}/simulate/authorization:
- post:
- summary: Simulate a card authorization
- description: |
- Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip.
-
- The decisioning outcome is controlled by the last three characters of `merchant.descriptor`:
-
- | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved |
-
- Production returns `404` on this path.
- operationId: sandboxSimulateCardAuthorization
- tags:
- - Sandbox
- security:
- - BasicAuth: []
- parameters:
- - name: id
- in: path
- required: true
- description: The id of the card to simulate an authorization against.
- schema:
- type: string
- example: Card:019542f5-b3e7-1d02-0000-000000000010
- requestBody:
+ /agents/device-codes/{code}/redeem:
+ parameters:
+ - name: code
+ in: path
+ description: The device code to redeem
required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SandboxCardAuthorizationRequest'
- examples:
- coffeeAuth:
- summary: Approved $12.50 auth at a coffee shop
- value:
- amount: 1250
- currency:
- code: USD
- merchant:
- descriptor: BLUE BOTTLE COFFEE SF
- mcc: '5814'
- country: US
- declinedInsufficientFunds:
- summary: Declined — insufficient funds (descriptor suffix `002`)
- value:
- amount: 50000
- currency:
- code: USD
- merchant:
- descriptor: AMAZON RETAIL US-002
- mcc: '5942'
- country: US
+ schema:
+ type: string
+ post:
+ summary: Redeem device code
+ description: |
+ Redeem a device code to obtain agent credentials. This endpoint is called by the agent software during installation. On success, returns a Bearer access token that the agent uses for all subsequent API calls. The token is returned only once and must be stored securely.
+ This endpoint does not require platform authentication — the device code itself serves as proof of authorization.
+ operationId: redeemAgentDeviceCode
+ tags:
+ - Agent Management
+ security: []
responses:
'200':
- description: Simulated authorization processed. Returns the resulting card transaction.
+ description: Device code redeemed successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/CardTransaction'
+ $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request (e.g., code already redeemed or expired)
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Card not found (also returned in production for this path)
+ description: Device code not found
content:
application/json:
schema:
@@ -7499,54 +7101,99 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/cards/{id}/simulate/clearing:
- post:
- summary: Simulate a card clearing
+ /agents/me/transactions:
+ get:
+ summary: List agent transactions
description: |
- Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment.
-
- - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth).
- - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted.
- - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases).
-
- Production returns `404` on this path.
- operationId: sandboxSimulateCardClearing
+ Retrieve a paginated list of transactions for the authenticated agent's customer. Results are automatically scoped to the agent's associated customer — no customer filter is needed or accepted.
+ operationId: agentListTransactions
tags:
- - Sandbox
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
parameters:
- - name: id
- in: path
- required: true
- description: The id of the card the clearing applies to.
+ - name: accountIdentifier
+ in: query
+ description: Filter by account identifier (matches either sender or receiver)
+ required: false
schema:
type: string
- example: Card:019542f5-b3e7-1d02-0000-000000000010
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SandboxCardClearingRequest'
- examples:
- tipOnTopClearing:
- summary: Clearing larger than auth — exercises post-hoc pull
- value:
- cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
- amount: 1500
- authorizationExpiry:
- summary: Clearing of 0 — exercises authorization expiry
- value:
- cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
- amount: 0
+ - name: senderAccountIdentifier
+ in: query
+ description: Filter by sender account identifier
+ required: false
+ schema:
+ type: string
+ - name: receiverAccountIdentifier
+ in: query
+ description: Filter by receiver account identifier
+ required: false
+ schema:
+ type: string
+ - name: status
+ in: query
+ description: Filter by transaction status
+ required: false
+ schema:
+ $ref: '#/components/schemas/TransactionStatus'
+ - name: type
+ in: query
+ description: Filter by transaction type
+ required: false
+ schema:
+ $ref: '#/components/schemas/TransactionType'
+ - name: reference
+ in: query
+ description: Filter by reference
+ required: false
+ schema:
+ type: string
+ - name: startDate
+ in: query
+ description: Filter by start date (inclusive) in ISO 8601 format
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: endDate
+ in: query
+ description: Filter by end date (inclusive) in ISO 8601 format
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
responses:
'200':
- description: Simulated clearing processed. Returns the updated card transaction.
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/CardTransaction'
+ $ref: '#/components/schemas/TransactionListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -7559,14 +7206,44 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error403'
+ $ref: '#/components/schemas/Error500'
+ /agents/me/transactions/{transactionId}:
+ parameters:
+ - name: transactionId
+ in: path
+ description: Unique identifier of the transaction
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get agent transaction by ID
+ description: |
+ Retrieve a specific transaction belonging to the authenticated agent's customer. Returns 404 if the transaction exists but belongs to a different customer.
+ operationId: agentGetTransaction
+ tags:
+ - Agent Operations
+ security:
+ - AgentAuth: []
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TransactionOneOf'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
'404':
- description: Card or card transaction not found
+ description: Transaction not found
content:
application/json:
schema:
@@ -7577,47 +7254,41 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/cards/{id}/simulate/return:
+ /agents/me/quotes:
post:
- summary: Simulate a card return
+ summary: Create a transfer quote
description: |
- Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund).
-
- Production returns `404` on this path.
- operationId: sandboxSimulateCardReturn
+ Generate a quote for a cross-currency transfer on behalf of the authenticated agent's customer. Accounts referenced in the request must belong to the agent's customer. Requires the CREATE_QUOTES permission in the agent's policy.
+ If the agent's defaultExecutionMode is APPROVAL_REQUIRED, or the quote amount exceeds the agent's approvalThresholds, the resulting transaction will require explicit approval before funds move.
+ operationId: agentCreateQuote
tags:
- - Sandbox
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
parameters:
- - name: id
- in: path
- required: true
- description: The id of the card the return applies to.
- schema:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
type: string
- example: Card:019542f5-b3e7-1d02-0000-000000000010
+ example:
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/SandboxCardReturnRequest'
- examples:
- fullRefund:
- summary: Full refund of a $15.00 settled transaction
- value:
- cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
- amount: 1500
+ $ref: '#/components/schemas/QuoteRequest'
responses:
- '200':
- description: Simulated return processed. Returns the updated card transaction.
+ '201':
+ description: Transfer quote created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/CardTransaction'
+ $ref: '#/components/schemas/Quote'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request - Missing or invalid parameters
content:
application/json:
schema:
@@ -7629,13 +7300,61 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'403':
- description: Forbidden - request was made with a production platform token
+ description: Forbidden - Agent policy does not permit this operation
content:
application/json:
schema:
$ref: '#/components/schemas/Error403'
+ '412':
+ description: Counterparty doesn't support UMA version
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error412'
+ '424':
+ description: Counterparty issue
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error424'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /agents/me/quotes/{quoteId}:
+ parameters:
+ - name: quoteId
+ in: path
+ description: ID of the quote to retrieve
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get agent quote by ID
+ description: |
+ Retrieve a quote created by the authenticated agent. Returns 404 if the quote exists but was not created by this agent.
+ operationId: agentGetQuote
+ tags:
+ - Agent Operations
+ security:
+ - AgentAuth: []
+ responses:
+ '200':
+ description: Quote retrieved successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Quote'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
'404':
- description: Card or card transaction not found
+ description: Quote not found
content:
application/json:
schema:
@@ -7646,51 +7365,75 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /stablecoin-provider-accounts:
+ /agents/me/quotes/{quoteId}/execute:
+ parameters:
+ - name: quoteId
+ in: path
+ required: true
+ description: The unique identifier of the quote to execute
+ schema:
+ type: string
+ example: Quote:019542f5-b3e7-1d02-0000-000000000001
post:
- summary: Link a stablecoin provider account
+ summary: Execute a quote
description: |
- Link provider API credentials for the authenticated platform. Provider credentials are account-scoped and can be reused for multiple stablecoins. Currently, the only supported provider is `BRALE`; the provider environment is derived from the authenticated Grid platform mode.
- operationId: linkStablecoinProviderAccount
+ Execute a quote created by the authenticated agent. Requires the EXECUTE_QUOTES permission in the agent's policy.
+ If the agent's policy requires approval for this amount (based on execution mode or approval thresholds), the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
+ Once executed, the quote cannot be cancelled.
+ operationId: agentExecuteQuote
tags:
- - Stablecoins
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
parameters:
- name: Idempotency-Key
in: header
- description: Idempotency key for retrying this request safely. Replays return the prior result; conflicting payloads are rejected.
- required: true
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
schema:
type: string
- maxLength: 255
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/StablecoinProviderAccountLinkRequest'
+ example:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
responses:
- '201':
- description: Stablecoin provider account linked
+ '200':
+ description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.'
content:
application/json:
schema:
- $ref: '#/components/schemas/StablecoinProviderAccount'
+ $ref: '#/components/schemas/AgentAction'
'400':
- description: Bad request
+ description: Bad request - Quote cannot be executed
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Quote not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'409':
- description: Conflict
+ description: Conflict - Quote already executed, expired, or in invalid state
content:
application/json:
schema:
@@ -7701,25 +7444,23 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /agents/me/actions:
get:
- summary: List stablecoin provider account links
- description: Retrieve stablecoin provider account links for the authenticated platform.
- operationId: listStablecoinProviderAccounts
+ summary: List agent's own actions
+ description: |
+ Retrieve a paginated list of actions submitted by the authenticated agent. Use this to poll for approval decisions after submitting an action that requires approval.
+ operationId: agentListActions
tags:
- - Stablecoins
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
parameters:
- - name: provider
- in: query
- required: false
- schema:
- $ref: '#/components/schemas/StablecoinProvider'
- name: status
in: query
+ description: Filter by action status
required: false
schema:
- $ref: '#/components/schemas/StablecoinProviderAccountStatus'
+ $ref: '#/components/schemas/AgentActionStatus'
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -7741,13 +7482,7 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/StablecoinProviderAccountListResponse'
- '400':
- description: Bad request - Invalid parameters
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/AgentActionListResponse'
'401':
description: Unauthorized
content:
@@ -7760,29 +7495,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /stablecoin-provider-accounts/{stablecoinProviderAccountId}:
+ /agents/me/actions/{actionId}:
parameters:
- - name: stablecoinProviderAccountId
+ - name: actionId
in: path
- description: System-generated stablecoin provider account link identifier
+ description: Unique identifier of the agent action
required: true
schema:
type: string
get:
- summary: Get a stablecoin provider account link
- description: Retrieve a stablecoin provider account link by id for the authenticated platform.
- operationId: getStablecoinProviderAccount
+ summary: Get an agent action
+ description: |
+ Retrieve a specific action submitted by the authenticated agent. Poll this endpoint after submitting an action that requires approval to check whether it has been approved, rejected, or has failed.
+ operationId: agentGetAction
tags:
- - Stablecoins
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/StablecoinProviderAccount'
+ $ref: '#/components/schemas/AgentAction'
'401':
description: Unauthorized
content:
@@ -7790,7 +7526,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Stablecoin provider account link not found
+ description: Action not found
content:
application/json:
schema:
@@ -7801,580 +7537,316 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
-webhooks:
- agent-action:
+ /agents/me/transfer-in:
post:
- summary: Agent action pending approval webhook
+ summary: Create a transfer-in
description: |
- Fired when an agent submits an action that requires platform approval before Grid will execute it. Use this to send a push notification to the customer so they can review and approve or reject the action in your app.
- This endpoint should be implemented by clients of the Grid API.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- The payload contains the full `AgentAction` — including the embedded quote or transfer details — so you can render the approval UI without a second API call. Approve or reject via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`.
- operationId: agentActionWebhook
+ Transfer funds from an external account to an internal account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy.
+ If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
+ This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the payment instructions on the internal account to deposit funds.
+ operationId: agentCreateTransferIn
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
+ - AgentAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentActionWebhook'
+ $ref: '#/components/schemas/TransferInRequest'
examples:
- pendingApproval:
- summary: Agent action pending approval
+ transferIn:
+ summary: Transfer from external to internal account
value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000020
- type: AGENT_ACTION.PENDING_APPROVAL
- timestamp: '2025-10-03T15:00:00Z'
- data:
- id: AgentAction:019542f5-b3e7-1d02-0000-000000000099
- agentId: Agent:019542f5-b3e7-1d02-0000-000000000042
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000010
- platformCustomerId: user-a1b2c3
- status: PENDING_APPROVAL
- type: EXECUTE_QUOTE
- quote:
- id: Quote:019542f5-b3e7-1d02-0000-000000000006
- status: PENDING
- expiresAt: '2025-10-03T15:00:30Z'
- createdAt: '2025-10-03T15:00:00Z'
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- sendingCurrency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- receivingCurrency:
- code: INR
- name: Indian Rupee
- symbol: ₹
- decimals: 2
- totalSendingAmount: 50000
- totalReceivingAmount: 4625000
- exchangeRate: 92.5
- feesIncluded: 250
- transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000099
- createdAt: '2025-10-03T15:00:00Z'
- updatedAt: '2025-10-03T15:00:00Z'
+ source:
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ amount: 12550
responses:
- '200':
- description: Webhook received and acknowledged.
+ '201':
+ description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentAction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ '403':
+ description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- incoming-payment:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /agents/me/transfer-out:
post:
- summary: Incoming payment webhook and approval mechanism
+ summary: Create a transfer-out
description: |
- Webhook that is called when an incoming payment is received by a customer's UMA address.
- This endpoint should be implemented by clients of the Grid API.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- ### Payment Approval Flow
- When a transaction has `status: "PENDING"`, this webhook serves as an approval mechanism:
-
- 1. The client should check the `counterpartyInformation` against their requirements
- 2. To APPROVE the payment synchronously, return a 200 OK response
- 3. To REJECT the payment, return a 403 Forbidden response with an Error object
- 4. To request more information, return a 422 Unprocessable Entity with specific missing fields
- 5. To process the payment asynchronously, return a 202 Accepted response and then call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint within 5 seconds. Note that synchronous approval/rejection is preferred where possible.
-
- The Grid system will proceed or cancel the payment based on your response.
-
- For transactions with other statuses (COMPLETED, FAILED, REFUNDED), this webhook is purely informational.
- operationId: incomingPaymentWebhook
+ Transfer funds from an internal account to an external account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy.
+ If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
+ operationId: agentCreateTransferOut
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
+ - AgentAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingPaymentWebhook'
+ $ref: '#/components/schemas/TransferOutRequest'
examples:
- pendingPayment:
- summary: Pending payment example requiring approval
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000007
- type: INCOMING_PAYMENT.PENDING
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000005
- status: PENDING
- type: INCOMING
- direction: CREDIT
- destination:
- destinationType: UMA_ADDRESS
- umaAddress: $recipient@uma.domain
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- senderUmaAddress: $sender@external.domain
- receiverUmaAddress: $recipient@uma.domain
- receivedAmount:
- amount: 50000
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- counterpartyInformation:
- FULL_NAME: John Sender
- BIRTH_DATE: '1985-06-15'
- NATIONALITY: US
- reconciliationInstructions:
- reference: REF-123456789
- requestedReceiverCustomerInfoFields:
- - name: NATIONALITY
- mandatory: true
- - name: POSTAL_ADDRESS
- mandatory: false
- incomingCompletedPayment:
- summary: Completed payment notification
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000007
- type: INCOMING_PAYMENT.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000005
- status: COMPLETED
- type: INCOMING
- direction: CREDIT
- destination:
- destinationType: UMA_ADDRESS
- umaAddress: $recipient@uma.domain
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- senderUmaAddress: $sender@external.domain
- receiverUmaAddress: $recipient@uma.domain
- receivedAmount:
- amount: 50000
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- settledAt: '2025-08-15T14:30:00Z'
- createdAt: '2025-08-15T14:25:18Z'
- description: Payment for services
- reconciliationInstructions:
- reference: REF-123456789
- incomingCompletedCryptoPayment:
- summary: Completed payment funded from an external crypto wallet
+ transferOut:
+ summary: Transfer from internal to external account
value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000009
- type: INCOMING_PAYMENT.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000006
- status: COMPLETED
- type: INCOMING
- direction: CREDIT
- source:
- sourceType: REALTIME_FUNDING
- currency: USDC
- onChainTransaction:
- transactionHash: 7RJWhvQBQPEjJmki5fhBboGBWRJhmcFkMvrr4Fu3tMSJ5EdynMEiYSyiWAH9GpcbHpeUzeSQF9ZY6q4x8AhBskUf
- network: SOLANA
- destination:
- destinationType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- receivedAmount:
- amount: 100000
- currency:
- code: USDC
- name: USD Coin
- symbol: ''
- decimals: 6
- settledAt: '2025-08-15T14:30:00Z'
- createdAt: '2025-08-15T14:25:18Z'
- description: USDC deposit from self-custody wallet
+ source:
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ destination:
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ amount: 12550
responses:
- '200':
- description: |
- Webhook received successfully.
- For PENDING transactions, this indicates approval to proceed with the payment.
- If `requestedReceiverCustomerInfoFields` were present in the webhook request, the corresponding fields for the recipient must be included in this response in the `receiverCustomerInfo` object.
+ '201':
+ description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated.
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingPaymentWebhookResponse'
- '202':
- description: |
- Webhook received and will be processed asynchronously. The synchronous 200 response should be preferred where possible. This asycnhronous path should only be used in
- cases where the platform's architecture requires async (but still very quick) processing before approving or rejecting the payment.
- The platform must call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint to approve or reject the payment within 5 seconds or the payment will be automatically rejected.
+ $ref: '#/components/schemas/AgentAction'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'403':
- description: |
- Forbidden - Payment rejected by the client.
- Only applicable for PENDING transactions.
+ description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Account not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- '422':
- description: |
- Unprocessable Entity - Additional counterparty information required.
- Only applicable for PENDING transactions.
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingPaymentWebhookUnprocessableResponse'
- outgoing-payment:
- post:
- summary: Outgoing payment status webhook
+ $ref: '#/components/schemas/Error500'
+ /agents/me/internal-accounts:
+ get:
+ summary: List agent's internal accounts
description: |
- Webhook that is called when an outgoing payment's status changes.
- This endpoint should be implemented by clients of the Grid API.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded.
- operationId: outgoingPaymentWebhook
+ Retrieve the internal accounts belonging to the customer this agent operates on behalf of. Use this to discover available source accounts for transfers and quotes, and to verify which accounts are accessible under the agent's `accountRestrictions` policy.
+ operationId: agentListInternalAccounts
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/OutgoingPaymentWebhook'
- examples:
- outgoingCompletedPayment:
- summary: Completed outgoing payment
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000007
- type: OUTGOING_PAYMENT.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000005
- status: COMPLETED
- type: OUTGOING
- direction: DEBIT
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- senderUmaAddress: $sender@uma.domain
- receiverUmaAddress: $recipient@external.domain
- sentAmount:
- amount: 10550
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- receivedAmount:
- amount: 9706
- currency:
- code: EUR
- name: Euro
- symbol: €
- decimals: 2
- exchangeRate: 0.92
- quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006
- settledAt: '2025-08-15T14:30:00Z'
- createdAt: '2025-08-15T14:25:18Z'
- description: 'Payment for invoice #1234'
- paymentInstructions: []
- rateDetails:
- counterpartyMultiplier: 1.08
- counterpartyFixedFee: 10
- gridApiMultiplier: 0.925
- gridApiFixedFee: 10
- gridApiVariableFeeRate: 0.003
- gridApiVariableFeeAmount: 30
- outgoingCompletedCryptoPayment:
- summary: Completed crypto payout to an external wallet
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000008
- type: OUTGOING_PAYMENT.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000009
- status: COMPLETED
- type: OUTGOING
- direction: DEBIT
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:c34dcbd6-dced-4ec4-b756-3c3a9ea3d789
- onChainTransaction:
- transactionHash: h82pJGF9p7kpzb6eU326EFZf2cDnimbTFVeJtx1qtBmUNJAEqN76R7PwPfHt3oWb8R6cKvhgyxQdDn53jFrK6wFx
- network: SOLANA
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- sentAmount:
- amount: 100000
- currency:
- code: USDC
- name: USD Coin
- symbol: ''
- decimals: 6
- receivedAmount:
- amount: 100000
- currency:
- code: USDC
- name: USD Coin
- symbol: ''
- decimals: 6
- quoteId: Quote:019542f5-b3e7-1d02-0000-000000000010
- settledAt: '2025-08-15T14:30:00Z'
- createdAt: '2025-08-15T14:25:18Z'
- description: USDC withdrawal to self-custody wallet
- paymentInstructions: []
- failedPayment:
- summary: Failed outgoing payment
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000007
- type: OUTGOING_PAYMENT.FAILED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000005
- status: FAILED
- type: OUTGOING
- direction: DEBIT
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- senderUmaAddress: $sender@uma.domain
- receiverUmaAddress: $recipient@external.domain
- sentAmount:
- amount: 10550
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- createdAt: '2025-08-15T14:25:18Z'
- quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006
- failureReason: QUOTE_EXECUTION_FAILED
+ - AgentAuth: []
+ parameters:
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
+ schema:
+ type: string
+ - name: type
+ in: query
+ description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for the customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for platform-managed holding accounts.
+ required: false
+ schema:
+ $ref: '#/components/schemas/InternalAccountType'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Webhook received successfully
- '400':
- description: Bad request
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/InternalAccountListResponse'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- test-webhook:
- post:
- summary: Test webhook for integration verification
+ $ref: '#/components/schemas/Error500'
+ /agents/me/external-accounts:
+ get:
+ summary: List agent external accounts
description: |
- Webhook that is sent once to verify your webhook endpoint is correctly set up.
- This is sent when you configure or update your platform settings with a webhook URL.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by the Grid API.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- This webhook is purely for testing your endpoint integration and signature verification.
- operationId: testWebhook
+ Retrieve a paginated list of external accounts belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
+ operationId: agentListExternalAccounts
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TestWebhookRequest'
- examples:
- testWebhook:
- summary: Test webhook example
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000001
- type: TEST
- timestamp: '2025-08-15T14:32:00Z'
- data: {}
+ - AgentAuth: []
+ parameters:
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
+ schema:
+ type: string
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Webhook received successfully. This confirms your webhook endpoint is properly configured.
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExternalAccountListResponse'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- bulk-upload:
+ $ref: '#/components/schemas/Error500'
post:
- summary: Bulk upload status webhook
+ summary: Add an external account
description: |
- Webhook that is called when a bulk customer upload job completes or fails.
- This endpoint should be implemented by clients of the Grid API.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- This webhook is sent when a bulk upload job completes or fails, providing detailed information about the results.
- operationId: bulkUploadWebhook
+ Register a new external bank account or wallet for the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. The `customerId` field is optional and will be inferred from the agent's associated customer if omitted.
+ operationId: agentCreateExternalAccount
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
+ - AgentAuth: []
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/BulkUploadWebhook'
+ $ref: '#/components/schemas/ExternalAccountCreateRequest'
examples:
- completedUpload:
- summary: Successful bulk upload completion
+ usBankAccount:
+ summary: Create external US bank account
value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000008
- type: BULK_UPLOAD.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Job:019542f5-b3e7-1d02-0000-000000000006
- status: COMPLETED
- progress:
- total: 5000
- processed: 5000
- successful: 5000
- failed: 0
- errors: []
- failedUpload:
- summary: Failed bulk upload
+ currency: USD
+ accountInfo:
+ accountType: USD_ACCOUNT
+ accountNumber: '12345678901'
+ routingNumber: '123456789'
+ bankAccountType: CHECKING
+ bankName: Chase Bank
+ beneficiary:
+ beneficiaryType: INDIVIDUAL
+ fullName: John Doe
+ birthDate: '1990-01-15'
+ nationality: US
+ address:
+ line1: 123 Main Street
+ city: San Francisco
+ state: CA
+ postalCode: '94105'
+ country: US
+ sparkWallet:
+ summary: Create external Spark wallet
value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000008
- type: BULK_UPLOAD.FAILED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Job:019542f5-b3e7-1d02-0000-000000000006
- status: FAILED
- progress:
- total: 5000
- processed: 5000
- successful: 0
- failed: 5000
- errors:
- - correlationId: row_1
- error:
- code: invalid_csv_format
- message: Invalid CSV format
- details:
- reason: missing_required_column
- column: umaAddress
+ currency: BTC
+ accountInfo:
+ accountType: SPARK_WALLET
+ address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
responses:
- '200':
- description: Webhook received successfully
+ '201':
+ description: External account created successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExternalAccount'
'400':
description: Bad request
content:
@@ -8382,121 +7854,1557 @@ webhooks:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ description: Conflict - External account already exists
content:
application/json:
schema:
$ref: '#/components/schemas/Error409'
- invitation-claimed:
- post:
- summary: Invitation claimed webhook
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /agents/me/external-accounts/{externalAccountId}:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: System-generated unique external account identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get agent external account by ID
description: |
- Webhook that is called when an invitation is claimed by a customer.
- This endpoint should be implemented by platform clients of the Grid API.
-
- When a customer claims an invitation, this webhook is triggered to notify the platform that:
- 1. The invitation has been successfully claimed
- 2. The invitee UMA address is now associated with the invitation
- 3. The invitation status has changed from PENDING to CLAIMED
-
- This allows platforms to:
- - Track invitation usage and conversion rates
- - Trigger onboarding flows for new customers who joined via invitation
- - Apply referral bonuses or rewards to the inviter
- - Update their UI to reflect the claimed status
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
- operationId: invitationClaimedWebhook
+ Retrieve an external account belonging to the authenticated agent's customer. Returns 404 if the account exists but belongs to a different customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
+ operationId: agentGetExternalAccount
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/InvitationClaimedWebhook'
- examples:
- claimedInvitation:
- summary: Invitation claimed notification
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000008
- type: INVITATION.CLAIMED
- timestamp: '2025-09-01T15:45:00Z'
- data:
- code: 019542f5
- createdAt: '2025-09-01T14:30:00Z'
- claimedAt: '2025-09-01T15:45:00Z'
- inviterUma: $inviter@uma.domain
- inviteeUma: $invitee@uma.domain
- status: CLAIMED
- url: https://uma.me/i/019542f5
+ - AgentAuth: []
responses:
'200':
- description: Webhook received successfully
- '400':
- description: Bad request
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/ExternalAccount'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ '404':
+ description: External account not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- customer-update:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ delete:
+ summary: Delete agent external account
+ description: |
+ Delete an external account belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
+ operationId: agentDeleteExternalAccount
+ tags:
+ - Agent Operations
+ security:
+ - AgentAuth: []
+ responses:
+ '204':
+ description: External account deleted successfully
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: External account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /cards:
post:
- summary: Customer status change
+ summary: Issue a card
description: |
- Webhook that is called when the status of a customer is updated, including KYC and KYB status changes.
- This endpoint should be implemented by clients of the Grid API.
+ Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`.
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid API public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
+ If any funding source is an Embedded Wallet internal account, the cardholder must authorize Grid to sign Spark token transactions for that card funding source by completing the delegated-key creation flow with `POST /auth/delegated-keys`. Until an active delegated key exists for that funding source, Authorization Decisioning cannot use it to fund card transactions.
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
- operationId: customerStatusWebhook
+ New cards start in `state: "PROCESSING"` while the card issuer provisions the card. The `card.state_change` webhook fires on each state transition, including the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails).
+ operationId: createCard
tags:
- - Webhooks
+ - Cards
security:
- - WebhookSignature: []
+ - BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/CustomerWebhook'
+ $ref: '#/components/schemas/CardCreateRequest'
examples:
- kycApprovedWebhook:
- summary: When an individual customer KYC has been approved
+ virtualCard:
+ summary: Issue a virtual card with one funding source
+ value:
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId: card-emp-aary-001
+ form: VIRTUAL
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ responses:
+ '201':
+ description: Card created successfully. Newly-created cards start in `PROCESSING` while the issuer provisions them. Cards funded by an Embedded Wallet internal account also require an active delegated key for that funding source before Authorization Decisioning can use it.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '400':
+ description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Card issuance is not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ get:
+ summary: List cards
+ description: |
+ Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller.
+ operationId: listCards
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: cardholderId
+ in: query
+ description: Filter by cardholder (customer) id.
+ required: false
+ schema:
+ type: string
+ - name: accountId
+ in: query
+ description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id.
+ required: false
+ schema:
+ type: string
+ - name: platformCardId
+ in: query
+ description: Filter by platform-specific card identifier.
+ required: false
+ schema:
+ type: string
+ - name: state
+ in: query
+ description: Filter by card state.
+ required: false
+ schema:
+ $ref: '#/components/schemas/CardState'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardListResponse'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ /cards/{id}:
+ parameters:
+ - name: id
+ in: path
+ description: System-generated unique card identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get a card
+ description: Retrieve a card by its system-generated id. To display the card's full PAN, CVV, and expiry to the cardholder, request a reveal with `POST /cards/{id}/reveal` — the card resource itself never carries the reveal URL.
+ operationId: getCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ patch:
+ summary: Update a card
+ description: |
+ Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied.
+
+ - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`.
+ - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`.
+
+ Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`):
+
+ 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`.
+
+ Effects:
+ - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed.
+ - `state: ACTIVE`: normal authorization behavior resumes.
+ - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys.
+ - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding.
+
+ The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated.
+ operationId: updateCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardUpdateRequest'
+ examples:
+ freeze:
+ summary: Freeze an active card
+ value:
+ state: FROZEN
+ unfreeze:
+ summary: Unfreeze a frozen card
+ value:
+ state: ACTIVE
+ updateFundingSources:
+ summary: Replace the card's bound funding sources
+ value:
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ freezeAndUpdateSources:
+ summary: Freeze the card and replace its funding sources in one call
+ value:
+ state: FROZEN
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ close:
+ summary: Permanently close the card
+ value:
+ state: CLOSED
+ responses:
+ '200':
+ description: Signed retry accepted. Returns the updated card.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '202':
+ description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SignedRequestChallenge'
+ '400':
+ description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ /cards/{id}/reveal:
+ parameters:
+ - name: id
+ in: path
+ description: System-generated unique card identifier
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Reveal card details
+ description: |-
+ Mint a signed, short-lived URL for the card processor's iframe that displays the card's full PAN, CVV, and expiry to the cardholder. This is the only way to obtain a reveal URL — the `Card` resource never carries one.
+
+ Request the reveal right before rendering the iframe and render the returned `panEmbedUrl` immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log the URL — it is a bearer secret for the full card details. The card data renders inside the processor's iframe and never crosses Grid's or your servers.
+
+ Every reveal is audit-logged with the requesting actor.
+ operationId: revealCard
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Reveal URL minted.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardRevealResponse'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden. The session has no attributable actor to audit the reveal against (for example, an impersonated dashboard session).
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ /sandbox/cards/{id}/simulate/authorization:
+ post:
+ summary: Simulate a card authorization
+ description: |
+ Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip.
+
+ The decisioning outcome is controlled by the last three characters of `merchant.descriptor`:
+
+ | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved |
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardAuthorization
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card to simulate an authorization against.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardAuthorizationRequest'
+ examples:
+ coffeeAuth:
+ summary: Approved $12.50 auth at a coffee shop
+ value:
+ amount: 1250
+ currency:
+ code: USD
+ merchant:
+ descriptor: BLUE BOTTLE COFFEE SF
+ mcc: '5814'
+ country: US
+ declinedInsufficientFunds:
+ summary: Declined — insufficient funds (descriptor suffix `002`)
+ value:
+ amount: 50000
+ currency:
+ code: USD
+ merchant:
+ descriptor: AMAZON RETAIL US-002
+ mcc: '5942'
+ country: US
+ responses:
+ '200':
+ description: Simulated authorization processed. Returns the resulting card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card not found (also returned in production for this path)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/clearing:
+ post:
+ summary: Simulate a card clearing
+ description: |
+ Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment.
+
+ - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth).
+ - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted.
+ - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases).
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardClearing
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the clearing applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardClearingRequest'
+ examples:
+ tipOnTopClearing:
+ summary: Clearing larger than auth — exercises post-hoc pull
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ authorizationExpiry:
+ summary: Clearing of 0 — exercises authorization expiry
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 0
+ responses:
+ '200':
+ description: Simulated clearing processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/return:
+ post:
+ summary: Simulate a card return
+ description: |
+ Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund).
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardReturn
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the return applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardReturnRequest'
+ examples:
+ fullRefund:
+ summary: Full refund of a $15.00 settled transaction
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ responses:
+ '200':
+ description: Simulated return processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /stablecoin-provider-accounts:
+ post:
+ summary: Link a stablecoin provider account
+ description: |
+ Link provider API credentials for the authenticated platform. Provider credentials are account-scoped and can be reused for multiple stablecoins. Currently, the only supported provider is `BRALE`; the provider environment is derived from the authenticated Grid platform mode.
+ operationId: linkStablecoinProviderAccount
+ tags:
+ - Stablecoins
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ description: Idempotency key for retrying this request safely. Replays return the prior result; conflicting payloads are rejected.
+ required: true
+ schema:
+ type: string
+ maxLength: 255
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccountLinkRequest'
+ responses:
+ '201':
+ description: Stablecoin provider account linked
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccount'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ get:
+ summary: List stablecoin provider account links
+ description: Retrieve stablecoin provider account links for the authenticated platform.
+ operationId: listStablecoinProviderAccounts
+ tags:
+ - Stablecoins
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: provider
+ in: query
+ required: false
+ schema:
+ $ref: '#/components/schemas/StablecoinProvider'
+ - name: status
+ in: query
+ required: false
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccountStatus'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccountListResponse'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /stablecoin-provider-accounts/{stablecoinProviderAccountId}:
+ parameters:
+ - name: stablecoinProviderAccountId
+ in: path
+ description: System-generated stablecoin provider account link identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get a stablecoin provider account link
+ description: Retrieve a stablecoin provider account link by id for the authenticated platform.
+ operationId: getStablecoinProviderAccount
+ tags:
+ - Stablecoins
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccount'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Stablecoin provider account link not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+webhooks:
+ agent-action:
+ post:
+ summary: Agent action pending approval webhook
+ description: |
+ Fired when an agent submits an action that requires platform approval before Grid will execute it. Use this to send a push notification to the customer so they can review and approve or reject the action in your app.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ The payload contains the full `AgentAction` — including the embedded quote or transfer details — so you can render the approval UI without a second API call. Approve or reject via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`.
+ operationId: agentActionWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentActionWebhook'
+ examples:
+ pendingApproval:
+ summary: Agent action pending approval
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000020
+ type: AGENT_ACTION.PENDING_APPROVAL
+ timestamp: '2025-10-03T15:00:00Z'
+ data:
+ id: AgentAction:019542f5-b3e7-1d02-0000-000000000099
+ agentId: Agent:019542f5-b3e7-1d02-0000-000000000042
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000010
+ platformCustomerId: user-a1b2c3
+ status: PENDING_APPROVAL
+ type: EXECUTE_QUOTE
+ quote:
+ id: Quote:019542f5-b3e7-1d02-0000-000000000006
+ status: PENDING
+ expiresAt: '2025-10-03T15:00:30Z'
+ createdAt: '2025-10-03T15:00:00Z'
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ sendingCurrency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ receivingCurrency:
+ code: INR
+ name: Indian Rupee
+ symbol: ₹
+ decimals: 2
+ totalSendingAmount: 50000
+ totalReceivingAmount: 4625000
+ exchangeRate: 92.5
+ feesIncluded: 250
+ transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000099
+ createdAt: '2025-10-03T15:00:00Z'
+ updatedAt: '2025-10-03T15:00:00Z'
+ responses:
+ '200':
+ description: Webhook received and acknowledged.
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ incoming-payment:
+ post:
+ summary: Incoming payment webhook and approval mechanism
+ description: |
+ Webhook that is called when an incoming payment is received by a customer's UMA address.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ ### Payment Approval Flow
+ When a transaction has `status: "PENDING"`, this webhook serves as an approval mechanism:
+
+ 1. The client should check the `counterpartyInformation` against their requirements
+ 2. To APPROVE the payment synchronously, return a 200 OK response
+ 3. To REJECT the payment, return a 403 Forbidden response with an Error object
+ 4. To request more information, return a 422 Unprocessable Entity with specific missing fields
+ 5. To process the payment asynchronously, return a 202 Accepted response and then call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint within 5 seconds. Note that synchronous approval/rejection is preferred where possible.
+
+ The Grid system will proceed or cancel the payment based on your response.
+
+ For transactions with other statuses (COMPLETED, FAILED, REFUNDED), this webhook is purely informational.
+ operationId: incomingPaymentWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IncomingPaymentWebhook'
+ examples:
+ pendingPayment:
+ summary: Pending payment example requiring approval
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000007
+ type: INCOMING_PAYMENT.PENDING
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ status: PENDING
+ type: INCOMING
+ direction: CREDIT
+ destination:
+ destinationType: UMA_ADDRESS
+ umaAddress: $recipient@uma.domain
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ senderUmaAddress: $sender@external.domain
+ receiverUmaAddress: $recipient@uma.domain
+ receivedAmount:
+ amount: 50000
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ counterpartyInformation:
+ FULL_NAME: John Sender
+ BIRTH_DATE: '1985-06-15'
+ NATIONALITY: US
+ reconciliationInstructions:
+ reference: REF-123456789
+ requestedReceiverCustomerInfoFields:
+ - name: NATIONALITY
+ mandatory: true
+ - name: POSTAL_ADDRESS
+ mandatory: false
+ incomingCompletedPayment:
+ summary: Completed payment notification
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000007
+ type: INCOMING_PAYMENT.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ status: COMPLETED
+ type: INCOMING
+ direction: CREDIT
+ destination:
+ destinationType: UMA_ADDRESS
+ umaAddress: $recipient@uma.domain
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ senderUmaAddress: $sender@external.domain
+ receiverUmaAddress: $recipient@uma.domain
+ receivedAmount:
+ amount: 50000
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ settledAt: '2025-08-15T14:30:00Z'
+ createdAt: '2025-08-15T14:25:18Z'
+ description: Payment for services
+ reconciliationInstructions:
+ reference: REF-123456789
+ incomingCompletedCryptoPayment:
+ summary: Completed payment funded from an external crypto wallet
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000009
+ type: INCOMING_PAYMENT.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000006
+ status: COMPLETED
+ type: INCOMING
+ direction: CREDIT
+ source:
+ sourceType: REALTIME_FUNDING
+ currency: USDC
+ onChainTransaction:
+ transactionHash: 7RJWhvQBQPEjJmki5fhBboGBWRJhmcFkMvrr4Fu3tMSJ5EdynMEiYSyiWAH9GpcbHpeUzeSQF9ZY6q4x8AhBskUf
+ network: SOLANA
+ destination:
+ destinationType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ receivedAmount:
+ amount: 100000
+ currency:
+ code: USDC
+ name: USD Coin
+ symbol: ''
+ decimals: 6
+ settledAt: '2025-08-15T14:30:00Z'
+ createdAt: '2025-08-15T14:25:18Z'
+ description: USDC deposit from self-custody wallet
+ responses:
+ '200':
+ description: |
+ Webhook received successfully.
+ For PENDING transactions, this indicates approval to proceed with the payment.
+ If `requestedReceiverCustomerInfoFields` were present in the webhook request, the corresponding fields for the recipient must be included in this response in the `receiverCustomerInfo` object.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IncomingPaymentWebhookResponse'
+ '202':
+ description: |
+ Webhook received and will be processed asynchronously. The synchronous 200 response should be preferred where possible. This asycnhronous path should only be used in
+ cases where the platform's architecture requires async (but still very quick) processing before approving or rejecting the payment.
+ The platform must call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint to approve or reject the payment within 5 seconds or the payment will be automatically rejected.
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: |
+ Forbidden - Payment rejected by the client.
+ Only applicable for PENDING transactions.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '422':
+ description: |
+ Unprocessable Entity - Additional counterparty information required.
+ Only applicable for PENDING transactions.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IncomingPaymentWebhookUnprocessableResponse'
+ outgoing-payment:
+ post:
+ summary: Outgoing payment status webhook
+ description: |
+ Webhook that is called when an outgoing payment's status changes.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded.
+ operationId: outgoingPaymentWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OutgoingPaymentWebhook'
+ examples:
+ outgoingCompletedPayment:
+ summary: Completed outgoing payment
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000007
+ type: OUTGOING_PAYMENT.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ status: COMPLETED
+ type: OUTGOING
+ direction: DEBIT
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ senderUmaAddress: $sender@uma.domain
+ receiverUmaAddress: $recipient@external.domain
+ sentAmount:
+ amount: 10550
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ receivedAmount:
+ amount: 9706
+ currency:
+ code: EUR
+ name: Euro
+ symbol: €
+ decimals: 2
+ exchangeRate: 0.92
+ quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006
+ settledAt: '2025-08-15T14:30:00Z'
+ createdAt: '2025-08-15T14:25:18Z'
+ description: 'Payment for invoice #1234'
+ paymentInstructions: []
+ rateDetails:
+ counterpartyMultiplier: 1.08
+ counterpartyFixedFee: 10
+ gridApiMultiplier: 0.925
+ gridApiFixedFee: 10
+ gridApiVariableFeeRate: 0.003
+ gridApiVariableFeeAmount: 30
+ outgoingCompletedCryptoPayment:
+ summary: Completed crypto payout to an external wallet
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000008
+ type: OUTGOING_PAYMENT.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000009
+ status: COMPLETED
+ type: OUTGOING
+ direction: DEBIT
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:c34dcbd6-dced-4ec4-b756-3c3a9ea3d789
+ onChainTransaction:
+ transactionHash: h82pJGF9p7kpzb6eU326EFZf2cDnimbTFVeJtx1qtBmUNJAEqN76R7PwPfHt3oWb8R6cKvhgyxQdDn53jFrK6wFx
+ network: SOLANA
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ sentAmount:
+ amount: 100000
+ currency:
+ code: USDC
+ name: USD Coin
+ symbol: ''
+ decimals: 6
+ receivedAmount:
+ amount: 100000
+ currency:
+ code: USDC
+ name: USD Coin
+ symbol: ''
+ decimals: 6
+ quoteId: Quote:019542f5-b3e7-1d02-0000-000000000010
+ settledAt: '2025-08-15T14:30:00Z'
+ createdAt: '2025-08-15T14:25:18Z'
+ description: USDC withdrawal to self-custody wallet
+ paymentInstructions: []
+ failedPayment:
+ summary: Failed outgoing payment
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000007
+ type: OUTGOING_PAYMENT.FAILED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ status: FAILED
+ type: OUTGOING
+ direction: DEBIT
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ senderUmaAddress: $sender@uma.domain
+ receiverUmaAddress: $recipient@external.domain
+ sentAmount:
+ amount: 10550
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ createdAt: '2025-08-15T14:25:18Z'
+ quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006
+ failureReason: QUOTE_EXECUTION_FAILED
+ responses:
+ '200':
+ description: Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ test-webhook:
+ post:
+ summary: Test webhook for integration verification
+ description: |
+ Webhook that is sent once to verify your webhook endpoint is correctly set up.
+ This is sent when you configure or update your platform settings with a webhook URL.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by the Grid API.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ This webhook is purely for testing your endpoint integration and signature verification.
+ operationId: testWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TestWebhookRequest'
+ examples:
+ testWebhook:
+ summary: Test webhook example
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000001
+ type: TEST
+ timestamp: '2025-08-15T14:32:00Z'
+ data: {}
+ responses:
+ '200':
+ description: Webhook received successfully. This confirms your webhook endpoint is properly configured.
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ bulk-upload:
+ post:
+ summary: Bulk upload status webhook
+ description: |
+ Webhook that is called when a bulk customer upload job completes or fails.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ This webhook is sent when a bulk upload job completes or fails, providing detailed information about the results.
+ operationId: bulkUploadWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BulkUploadWebhook'
+ examples:
+ completedUpload:
+ summary: Successful bulk upload completion
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000008
+ type: BULK_UPLOAD.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Job:019542f5-b3e7-1d02-0000-000000000006
+ status: COMPLETED
+ progress:
+ total: 5000
+ processed: 5000
+ successful: 5000
+ failed: 0
+ errors: []
+ failedUpload:
+ summary: Failed bulk upload
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000008
+ type: BULK_UPLOAD.FAILED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Job:019542f5-b3e7-1d02-0000-000000000006
+ status: FAILED
+ progress:
+ total: 5000
+ processed: 5000
+ successful: 0
+ failed: 5000
+ errors:
+ - correlationId: row_1
+ error:
+ code: invalid_csv_format
+ message: Invalid CSV format
+ details:
+ reason: missing_required_column
+ column: umaAddress
+ responses:
+ '200':
+ description: Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ invitation-claimed:
+ post:
+ summary: Invitation claimed webhook
+ description: |
+ Webhook that is called when an invitation is claimed by a customer.
+ This endpoint should be implemented by platform clients of the Grid API.
+
+ When a customer claims an invitation, this webhook is triggered to notify the platform that:
+ 1. The invitation has been successfully claimed
+ 2. The invitee UMA address is now associated with the invitation
+ 3. The invitation status has changed from PENDING to CLAIMED
+
+ This allows platforms to:
+ - Track invitation usage and conversion rates
+ - Trigger onboarding flows for new customers who joined via invitation
+ - Apply referral bonuses or rewards to the inviter
+ - Update their UI to reflect the claimed status
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+ operationId: invitationClaimedWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/InvitationClaimedWebhook'
+ examples:
+ claimedInvitation:
+ summary: Invitation claimed notification
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000008
+ type: INVITATION.CLAIMED
+ timestamp: '2025-09-01T15:45:00Z'
+ data:
+ code: 019542f5
+ createdAt: '2025-09-01T14:30:00Z'
+ claimedAt: '2025-09-01T15:45:00Z'
+ inviterUma: $inviter@uma.domain
+ inviteeUma: $invitee@uma.domain
+ status: CLAIMED
+ url: https://uma.me/i/019542f5
+ responses:
+ '200':
+ description: Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ customer-update:
+ post:
+ summary: Customer status change
+ description: |
+ Webhook that is called when the status of a customer is updated, including KYC and KYB status changes.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid API public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+ operationId: customerStatusWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CustomerWebhook'
+ examples:
+ kycApprovedWebhook:
+ summary: When an individual customer KYC has been approved
value:
id: Webhook:019542f5-b3e7-1d02-0000-000000000007
type: CUSTOMER.KYC_APPROVED
@@ -10110,7 +11018,131 @@ components:
example: OPERATING_REVENUE
BusinessInfoUpdate:
type: object
- description: Additional information for business entities
+ description: Additional information for business entities
+ properties:
+ legalName:
+ type: string
+ description: Legal name of the business
+ example: Acme Corporation, Inc.
+ doingBusinessAs:
+ type: string
+ description: Trade name or DBA name of the business, if different from the legal name
+ example: Acme
+ country:
+ type: string
+ description: Country of incorporation or registration (ISO 3166-1 alpha-2)
+ example: US
+ registrationNumber:
+ type: string
+ description: Business registration number
+ example: '5523041'
+ incorporatedOn:
+ type: string
+ format: date
+ description: Date of incorporation in ISO 8601 format (YYYY-MM-DD)
+ example: '2018-03-14'
+ entityType:
+ $ref: '#/components/schemas/EntityType'
+ taxId:
+ type: string
+ description: Tax identification number
+ example: 47-1234567
+ countriesOfOperation:
+ type: array
+ items:
+ type: string
+ description: List of countries where the business operates (ISO 3166-1 alpha-2)
+ example:
+ - US
+ businessType:
+ $ref: '#/components/schemas/BusinessType'
+ purposeOfAccount:
+ $ref: '#/components/schemas/PurposeOfAccount'
+ sourceOfFunds:
+ type: string
+ description: The primary source of funds for the business
+ example: Funds derived from customer payments for software services
+ expectedMonthlyTransactionCount:
+ type: string
+ enum:
+ - COUNT_UNDER_10
+ - COUNT_10_TO_100
+ - COUNT_100_TO_500
+ - COUNT_500_TO_1000
+ - COUNT_OVER_1000
+ description: Expected number of transactions per month
+ example: COUNT_100_TO_500
+ expectedMonthlyTransactionVolume:
+ type: string
+ enum:
+ - VOLUME_UNDER_10K
+ - VOLUME_10K_TO_100K
+ - VOLUME_100K_TO_1M
+ - VOLUME_1M_TO_10M
+ - VOLUME_OVER_10M
+ description: Expected total transaction volume per month in USD equivalent
+ example: VOLUME_100K_TO_1M
+ expectedRecipientJurisdictions:
+ type: array
+ items:
+ type: string
+ description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2)
+ example:
+ - US
+ naicsCode:
+ type: string
+ pattern: ^\d{2,6}$
+ description: NAICS code describing the nature of the business (2-6 digits)
+ example: '541511'
+ sourceOfFundsCategories:
+ type: array
+ items:
+ $ref: '#/components/schemas/SourceOfFundsCategory'
+ description: Structured source-of-funds categories for the business
+ example:
+ - OPERATING_REVENUE
+ sourceOfFundsOtherDescription:
+ type: string
+ minLength: 1
+ maxLength: 500
+ description: Description of the source of funds when OTHER is selected
+ example: Proceeds from a legal settlement
+ purposeOfAccountOtherDescription:
+ type: string
+ minLength: 1
+ maxLength: 500
+ description: Description of the account purpose when OTHER is selected
+ example: Escrow for equipment leases
+ expectedCounterpartyCountries:
+ type: array
+ items:
+ type: string
+ description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
+ example:
+ - US
+ BusinessCustomerFields:
+ type: object
+ required:
+ - customerType
+ properties:
+ customerType:
+ type: string
+ enum:
+ - BUSINESS
+ kybStatus:
+ $ref: '#/components/schemas/KybStatus'
+ address:
+ $ref: '#/components/schemas/Address'
+ businessInfo:
+ $ref: '#/components/schemas/BusinessInfoUpdate'
+ BusinessInfoResponse:
+ type: object
+ description: |
+ Business information returned on a customer. `taxId` and `incorporatedOn` are
+ required on creation but may be absent on legacy customers that pre-date the
+ requirement, so both are optional in responses.
+ required:
+ - legalName
properties:
legalName:
type: string
@@ -10157,84 +11189,309 @@ components:
expectedMonthlyTransactionCount:
type: string
enum:
- - COUNT_UNDER_10
- - COUNT_10_TO_100
- - COUNT_100_TO_500
- - COUNT_500_TO_1000
- - COUNT_OVER_1000
- description: Expected number of transactions per month
- example: COUNT_100_TO_500
- expectedMonthlyTransactionVolume:
+ - COUNT_UNDER_10
+ - COUNT_10_TO_100
+ - COUNT_100_TO_500
+ - COUNT_500_TO_1000
+ - COUNT_OVER_1000
+ description: Expected number of transactions per month
+ example: COUNT_100_TO_500
+ expectedMonthlyTransactionVolume:
+ type: string
+ enum:
+ - VOLUME_UNDER_10K
+ - VOLUME_10K_TO_100K
+ - VOLUME_100K_TO_1M
+ - VOLUME_1M_TO_10M
+ - VOLUME_OVER_10M
+ description: Expected total transaction volume per month in USD equivalent
+ example: VOLUME_100K_TO_1M
+ expectedRecipientJurisdictions:
+ type: array
+ items:
+ type: string
+ description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2)
+ example:
+ - US
+ naicsCode:
+ type: string
+ pattern: ^\d{2,6}$
+ description: NAICS code describing the nature of the business (2-6 digits)
+ example: '541511'
+ sourceOfFundsCategories:
+ type: array
+ items:
+ $ref: '#/components/schemas/SourceOfFundsCategory'
+ description: Structured source-of-funds categories for the business
+ example:
+ - OPERATING_REVENUE
+ sourceOfFundsOtherDescription:
+ type: string
+ minLength: 1
+ maxLength: 500
+ description: Description of the source of funds when OTHER is selected
+ example: Proceeds from a legal settlement
+ purposeOfAccountOtherDescription:
+ type: string
+ minLength: 1
+ maxLength: 500
+ description: Description of the account purpose when OTHER is selected
+ example: Escrow for equipment leases
+ expectedCounterpartyCountries:
+ type: array
+ items:
+ type: string
+ description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
+ example:
+ - US
+ BeneficialOwnerRole:
+ type: string
+ enum:
+ - UBO
+ - DIRECTOR
+ - COMPANY_OFFICER
+ - CONTROL_PERSON
+ - TRUSTEE
+ - GENERAL_PARTNER
+ description: Role of the beneficial owner within the business
+ example: UBO
+ IdentificationType:
+ type: string
+ enum:
+ - SSN
+ - ITIN
+ - EIN
+ - NON_US_TAX_ID
+ description: Type of tax identification
+ example: SSN
+ BeneficialOwnerPersonalInfo:
+ type: object
+ required:
+ - firstName
+ - lastName
+ - birthDate
+ - nationality
+ - address
+ - idType
+ - identifier
+ properties:
+ firstName:
+ type: string
+ description: First name of the individual
+ example: Jane
+ middleName:
+ type: string
+ description: Middle name of the individual
+ example: Marie
+ lastName:
+ type: string
+ description: Last name of the individual
+ example: Smith
+ birthDate:
+ type: string
+ format: date
+ description: Date of birth in ISO 8601 format (YYYY-MM-DD)
+ example: '1978-06-15'
+ nationality:
+ type: string
+ description: Country of nationality (ISO 3166-1 alpha-2)
+ example: US
+ email:
+ type: string
+ format: email
+ description: Email address of the individual
+ example: jane.smith@acmecorp.com
+ phoneNumber:
+ type: string
+ description: Phone number in E.164 format
+ example: '+14155550192'
+ pattern: ^\+[1-9]\d{1,14}$
+ address:
+ $ref: '#/components/schemas/Address'
+ idType:
+ $ref: '#/components/schemas/IdentificationType'
+ identifier:
+ type: string
+ description: The identification number or value
+ example: 123-45-6789
+ countryOfIssuance:
+ type: string
+ description: Country that issued the identification (ISO 3166-1 alpha-2)
+ example: US
+ BeneficialOwner:
+ type: object
+ required:
+ - id
+ - customerId
+ - roles
+ - ownershipPercentage
+ - personalInfo
+ - kycStatus
+ - createdAt
+ properties:
+ id:
+ type: string
+ description: Unique identifier for this beneficial owner
+ example: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000001
+ customerId:
+ type: string
+ description: The ID of the business customer this beneficial owner is associated with
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ roles:
+ type: array
+ items:
+ $ref: '#/components/schemas/BeneficialOwnerRole'
+ description: Roles of this person within the business
+ example:
+ - UBO
+ - DIRECTOR
+ ownershipPercentage:
+ type: integer
+ description: Percentage of ownership in the business (0-100)
+ minimum: 0
+ maximum: 100
+ example: 51
+ personalInfo:
+ $ref: '#/components/schemas/BeneficialOwnerPersonalInfo'
+ kycStatus:
+ $ref: '#/components/schemas/KycStatus'
+ createdAt:
+ type: string
+ format: date-time
+ description: When this beneficial owner was created
+ example: '2025-10-03T12:00:00Z'
+ updatedAt:
+ type: string
+ format: date-time
+ description: When this beneficial owner was last updated
+ example: '2025-10-03T12:00:00Z'
+ BusinessCustomer:
+ title: Business Customer
+ allOf:
+ - $ref: '#/components/schemas/Customer'
+ - $ref: '#/components/schemas/BusinessCustomerFields'
+ - type: object
+ properties:
+ businessInfo:
+ $ref: '#/components/schemas/BusinessInfoResponse'
+ beneficialOwners:
+ type: array
+ items:
+ $ref: '#/components/schemas/BeneficialOwner'
+ CustomerOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/IndividualCustomer'
+ - $ref: '#/components/schemas/BusinessCustomer'
+ discriminator:
+ propertyName: customerType
+ mapping:
+ INDIVIDUAL: '#/components/schemas/IndividualCustomer'
+ BUSINESS: '#/components/schemas/BusinessCustomer'
+ CustomerListResponse:
+ type: object
+ required:
+ - data
+ - hasMore
+ properties:
+ data:
+ type: array
+ description: List of customers matching the filter criteria
+ items:
+ $ref: '#/components/schemas/CustomerOneOf'
+ hasMore:
+ type: boolean
+ description: Indicates if more results are available beyond this page
+ nextCursor:
+ type: string
+ description: Cursor to retrieve the next page of results (only present if hasMore is true)
+ totalCount:
+ type: integer
+ description: Total number of customers matching the criteria (excluding pagination)
+ Error405:
+ type: object
+ required:
+ - message
+ - status
+ - code
+ properties:
+ status:
+ type: integer
+ enum:
+ - 405
+ description: HTTP status code
+ code:
type: string
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | METHOD_NOT_ALLOWED | The HTTP method is not supported for this endpoint |
enum:
- - VOLUME_UNDER_10K
- - VOLUME_10K_TO_100K
- - VOLUME_100K_TO_1M
- - VOLUME_1M_TO_10M
- - VOLUME_OVER_10M
- description: Expected total transaction volume per month in USD equivalent
- example: VOLUME_100K_TO_1M
- expectedRecipientJurisdictions:
- type: array
- items:
- type: string
- description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2)
- example:
- - US
- naicsCode:
+ - METHOD_NOT_ALLOWED
+ message:
type: string
- pattern: ^\d{2,6}$
- description: NAICS code describing the nature of the business (2-6 digits)
- example: '541511'
- sourceOfFundsCategories:
- type: array
- items:
- $ref: '#/components/schemas/SourceOfFundsCategory'
- description: Structured source-of-funds categories for the business
- example:
- - OPERATING_REVENUE
- sourceOfFundsOtherDescription:
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ CustomerCreateRequest:
+ type: object
+ required:
+ - customerType
+ properties:
+ platformCustomerId:
type: string
- minLength: 1
- maxLength: 500
- description: Description of the source of funds when OTHER is selected
- example: Proceeds from a legal settlement
- purposeOfAccountOtherDescription:
+ description: Platform-specific customer identifier. If not provided, one will be generated by the system.
+ example: 9f84e0c2a72c4fa
+ customerType:
+ $ref: '#/components/schemas/CustomerType'
+ region:
type: string
- minLength: 1
- maxLength: 500
- description: Description of the account purpose when OTHER is selected
- example: Escrow for equipment leases
- expectedCounterpartyCountries:
+ description: Country code (ISO 3166-1 alpha-2) representing the customer's regional identity. This determines the regulatory jurisdiction and KYC requirements for the customer. Required if the customer will use currencies with different KYC requirements across regions. A customer with accounts in multiple regions should be registered as separate customers. This field is immutable after creation.
+ example: US
+ currencies:
type: array
items:
type: string
- description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
+ description: List of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Required if the customer will use more than one sending currency, since the correct currencies cannot always be inferred. If not provided, currencies will be inferred from the customer's region. Some currency combinations may require separate customers — if so, the request will be rejected with details.
example:
- - US
- BusinessCustomerFields:
- type: object
- required:
- - customerType
- properties:
- customerType:
+ - USD
+ - USDC
+ email:
type: string
- enum:
- - BUSINESS
- kybStatus:
- $ref: '#/components/schemas/KybStatus'
- address:
- $ref: '#/components/schemas/Address'
- businessInfo:
- $ref: '#/components/schemas/BusinessInfoUpdate'
- BusinessInfoResponse:
+ format: email
+ description: Email address for the customer.
+ example: john.doe@example.com
+ phoneNumber:
+ type: string
+ pattern: ^\+[1-9]\d{1,14}$
+ description: Phone number for the customer in strict E.164 format.
+ example: '+14155551234'
+ umaAddress:
+ type: string
+ description: Optional UMA address identifier. If not provided during customer creation, one will be generated by the system. If provided during customer update, the UMA address will be updated to the provided value. This is an optional identifier to route payments to the customer. This is an optional identifier to route payments to the customer.
+ example: $john.doe@uma.domain.com
+ IndividualCustomerCreateRequest:
+ title: Individual Customer Create Request
+ allOf:
+ - $ref: '#/components/schemas/CustomerCreateRequest'
+ - $ref: '#/components/schemas/IndividualCustomerFields'
+ - type: object
+ properties:
+ idType:
+ $ref: '#/components/schemas/IdentificationType'
+ identifier:
+ type: string
+ writeOnly: true
+ description: The individual's tax identification number. Required to onboard the individual as a US account holder. Only SSN and ITIN are currently accepted for an individual account holder; other identification types are rejected. Write-only — never returned in customer responses.
+ example: 123-45-6789
+ BusinessInfo:
type: object
- description: |
- Business information returned on a customer. `taxId` and `incorporatedOn` are
- required on creation but may be absent on legacy customers that pre-date the
- requirement, so both are optional in responses.
+ description: Additional information required for business entities
required:
- legalName
+ - taxId
+ - incorporatedOn
properties:
legalName:
type: string
@@ -10336,454 +11593,621 @@ components:
description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
example:
- US
- BeneficialOwnerRole:
- type: string
- enum:
- - UBO
- - DIRECTOR
- - COMPANY_OFFICER
- - CONTROL_PERSON
- - TRUSTEE
- - GENERAL_PARTNER
- description: Role of the beneficial owner within the business
- example: UBO
- IdentificationType:
- type: string
- enum:
- - SSN
- - ITIN
- - EIN
- - NON_US_TAX_ID
- description: Type of tax identification
- example: SSN
- BeneficialOwnerPersonalInfo:
+ BusinessCustomerCreateRequest:
+ title: Business Customer Create Request
+ allOf:
+ - $ref: '#/components/schemas/CustomerCreateRequest'
+ - $ref: '#/components/schemas/BusinessCustomerFields'
+ - type: object
+ properties:
+ businessInfo:
+ $ref: '#/components/schemas/BusinessInfo'
+ CustomerCreateRequestOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/IndividualCustomerCreateRequest'
+ - $ref: '#/components/schemas/BusinessCustomerCreateRequest'
+ discriminator:
+ propertyName: customerType
+ mapping:
+ INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest'
+ BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest'
+ Error409:
+ type: object
+ required:
+ - message
+ - status
+ - code
+ properties:
+ status:
+ type: integer
+ enum:
+ - 409
+ description: HTTP status code
+ code:
+ type: string
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL | Transaction is not pending platform approval |
+ | UMA_ADDRESS_EXISTS | UMA address already exists |
+ | EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential |
+ | EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued |
+ | PASSKEY_ALREADY_ENROLLED | The customer already has an enrolled passkey factor; only one passkey per customer is supported. Delete the existing one before enrolling another |
+ | CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform |
+ enum:
+ - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL
+ - UMA_ADDRESS_EXISTS
+ - EMAIL_OTP_EMAIL_ALREADY_EXISTS
+ - EMAIL_OTP_CREDENTIAL_SET_CHANGED
+ - PASSKEY_ALREADY_ENROLLED
+ - CONFLICT
+ message:
+ type: string
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ Error404:
+ type: object
+ required:
+ - message
+ - status
+ - code
+ properties:
+ status:
+ type: integer
+ enum:
+ - 404
+ description: HTTP status code
+ code:
+ type: string
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | TRANSACTION_NOT_FOUND | Transaction not found |
+ | INVITATION_NOT_FOUND | Invitation not found |
+ | USER_NOT_FOUND | Customer not found |
+ | QUOTE_NOT_FOUND | Quote not found |
+ | LOOKUP_REQUEST_NOT_FOUND | Lookup request not found |
+ | TOKEN_NOT_FOUND | Token not found |
+ | BULK_UPLOAD_JOB_NOT_FOUND | Bulk upload job not found |
+ | REFERENCE_NOT_FOUND | Reference not found |
+ | UMA_NOT_FOUND | The UMA address is well-formed but no receiver exists at the counterparty VASP |
+ | STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND | Stablecoin provider account link not found |
+ enum:
+ - TRANSACTION_NOT_FOUND
+ - INVITATION_NOT_FOUND
+ - USER_NOT_FOUND
+ - QUOTE_NOT_FOUND
+ - LOOKUP_REQUEST_NOT_FOUND
+ - TOKEN_NOT_FOUND
+ - BULK_UPLOAD_JOB_NOT_FOUND
+ - REFERENCE_NOT_FOUND
+ - UMA_NOT_FOUND
+ - STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND
+ message:
+ type: string
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ Error410:
type: object
required:
- - firstName
- - lastName
- - birthDate
- - nationality
- - address
- - idType
- - identifier
+ - message
+ - status
+ - code
properties:
- firstName:
- type: string
- description: First name of the individual
- example: Jane
- middleName:
- type: string
- description: Middle name of the individual
- example: Marie
- lastName:
- type: string
- description: Last name of the individual
- example: Smith
- birthDate:
+ status:
+ type: integer
+ enum:
+ - 410
+ description: HTTP status code
+ code:
type: string
- format: date
- description: Date of birth in ISO 8601 format (YYYY-MM-DD)
- example: '1978-06-15'
- nationality:
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | CUSTOMER_DELETED | Customer has been permanently deleted |
+ enum:
+ - CUSTOMER_DELETED
+ message:
type: string
- description: Country of nationality (ISO 3166-1 alpha-2)
- example: US
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ CustomerUpdateRequest:
+ title: Customer Update Request
+ description: Request body for `PATCH /customers/{customerId}`. When `email` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer email and every tied `EMAIL_OTP` credential through the endpoint's signed-retry flow. When `phoneNumber` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer phone number and every tied `SMS_OTP` credential through the same signed-retry flow. Update `email` and `phoneNumber` in separate PATCH calls.
+ type: object
+ required:
+ - customerType
+ properties:
+ customerType:
+ $ref: '#/components/schemas/CustomerType'
+ currencies:
+ type: array
+ items:
+ type: string
+ description: Updated list of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Replaces the existing list. Some currency combinations may require separate customers — if so, the request will be rejected with details.
+ example:
+ - USD
+ - EUR
+ - USDC
email:
type: string
format: email
- description: Email address of the individual
- example: jane.smith@acmecorp.com
+ description: Email address for the customer. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `EMAIL_OTP` credential across all tied Embedded Wallets.
+ example: john.doe@example.com
phoneNumber:
type: string
- description: Phone number in E.164 format
- example: '+14155550192'
pattern: ^\+[1-9]\d{1,14}$
- address:
- $ref: '#/components/schemas/Address'
- idType:
- $ref: '#/components/schemas/IdentificationType'
- identifier:
- type: string
- description: The identification number or value
- example: 123-45-6789
- countryOfIssuance:
+ description: Phone number for the customer in strict E.164 format. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `SMS_OTP` credential across all tied Embedded Wallets. Send phone number and email updates as separate PATCH calls.
+ example: '+14155551234'
+ umaAddress:
type: string
- description: Country that issued the identification (ISO 3166-1 alpha-2)
- example: US
- BeneficialOwner:
+ description: Optional UMA address identifier. If provided, the customer's UMA address will be updated. This is an optional identifier to route payments to the customer.
+ example: $john.doe@uma.domain.com
+ IndividualCustomerUpdateRequest:
+ title: Individual Customer Update Request
+ allOf:
+ - $ref: '#/components/schemas/CustomerUpdateRequest'
+ - $ref: '#/components/schemas/IndividualCustomerFields'
+ BusinessCustomerUpdateRequest:
+ title: Business Customer Update Request
+ allOf:
+ - $ref: '#/components/schemas/CustomerUpdateRequest'
+ - $ref: '#/components/schemas/BusinessCustomerFields'
+ CustomerUpdateRequestOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/IndividualCustomerUpdateRequest'
+ - $ref: '#/components/schemas/BusinessCustomerUpdateRequest'
+ discriminator:
+ propertyName: customerType
+ mapping:
+ INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest'
+ BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest'
+ SignedRequestChallenge:
+ title: Signed Request Challenge
type: object
required:
- - id
- - customerId
- - roles
- - ownershipPercentage
- - personalInfo
- - kycStatus
- - createdAt
+ - payloadToSign
+ - requestId
+ - expiresAt
+ description: Common base for two-step signed-retry challenge responses on Embedded Wallet endpoints (credential registration or revocation, session refresh or revocation, wallet export, customer email updates, and similar). Holds the signing fields shared across every challenge shape; each variant composes this base via `allOf` and adds its own resource `id` (and `type`, when applicable) with variant-specific description and example.
properties:
- id:
+ payloadToSign:
type: string
- description: Unique identifier for this beneficial owner
- example: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000001
- customerId:
+ description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request.
+ example: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_EXAMPLE"}'
+ requestId:
type: string
- description: The ID of the business customer this beneficial owner is associated with
- example: Customer:019542f5-b3e7-1d02-0000-000000000001
- roles:
- type: array
- items:
- $ref: '#/components/schemas/BeneficialOwnerRole'
- description: Roles of this person within the business
- example:
- - UBO
- - DIRECTOR
- ownershipPercentage:
- type: integer
- description: Percentage of ownership in the business (0-100)
- minimum: 0
- maximum: 100
- example: 51
- personalInfo:
- $ref: '#/components/schemas/BeneficialOwnerPersonalInfo'
- kycStatus:
- $ref: '#/components/schemas/KycStatus'
- createdAt:
+ description: Grid-issued `Request:` identifier for this pending request. Echo this value exactly in the `Request-Id` header on the signed retry so the server can correlate the retry with the issued challenge.
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt:
type: string
format: date-time
- description: When this beneficial owner was created
- example: '2025-10-03T12:00:00Z'
- updatedAt:
+ description: Timestamp after which this challenge is no longer valid. The signed retry must be submitted before this time.
+ example: '2026-04-08T15:35:00Z'
+ Error424:
+ type: object
+ required:
+ - message
+ - status
+ - code
+ properties:
+ status:
+ type: integer
+ enum:
+ - 424
+ description: HTTP status code
+ code:
type: string
- format: date-time
- description: When this beneficial owner was last updated
- example: '2025-10-03T12:00:00Z'
- BusinessCustomer:
- title: Business Customer
- allOf:
- - $ref: '#/components/schemas/Customer'
- - $ref: '#/components/schemas/BusinessCustomerFields'
- - type: object
- properties:
- businessInfo:
- $ref: '#/components/schemas/BusinessInfoResponse'
- beneficialOwners:
- type: array
- items:
- $ref: '#/components/schemas/BeneficialOwner'
- CustomerOneOf:
- oneOf:
- - $ref: '#/components/schemas/IndividualCustomer'
- - $ref: '#/components/schemas/BusinessCustomer'
- discriminator:
- propertyName: customerType
- mapping:
- INDIVIDUAL: '#/components/schemas/IndividualCustomer'
- BUSINESS: '#/components/schemas/BusinessCustomer'
- CustomerListResponse:
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | PAYREQ_REQUEST_FAILED | Payment request failed |
+ | COUNTERPARTY_PUBKEY_FETCH_ERROR | Error fetching counterparty public key |
+ | NO_COMPATIBLE_UMA_VERSION | No compatible UMA version |
+ | LNURLP_REQUEST_FAILED | LNURLP request failed |
+ | EMAIL_OTP_CREDENTIAL_SYNC_FAILED | Failed to update one or more tied EMAIL_OTP credentials |
+ enum:
+ - PAYREQ_REQUEST_FAILED
+ - COUNTERPARTY_PUBKEY_FETCH_ERROR
+ - NO_COMPATIBLE_UMA_VERSION
+ - LNURLP_REQUEST_FAILED
+ - EMAIL_OTP_CREDENTIAL_SYNC_FAILED
+ message:
+ type: string
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ KycLinkCreateRequest:
+ type: object
+ description: Request body for generating a hosted KYC link for an existing customer.
+ properties:
+ redirectUri:
+ type: string
+ format: uri
+ description: URI the customer is redirected to after completing the hosted KYC flow. Must start with `https://` (or `http://` for local development). Embedded in the returned `kycUrl`.
+ example: https://app.example.com/onboarding/completed
+ KycProvider:
+ type: string
+ description: The KYC provider that will perform identity verification for the customer. Grid selects the provider based on the customer's region and platform configuration; the value is informational for platforms that want to integrate directly with the provider's SDK.
+ enum:
+ - SUMSUB
+ example: SUMSUB
+ KycLinkResponse:
type: object
+ description: A hosted KYC link that the customer can complete to verify their identity.
required:
- - data
- - hasMore
+ - kycUrl
+ - expiresAt
+ - provider
properties:
- data:
- type: array
- description: List of customers matching the filter criteria
- items:
- $ref: '#/components/schemas/CustomerOneOf'
- hasMore:
- type: boolean
- description: Indicates if more results are available beyond this page
- nextCursor:
+ kycUrl:
type: string
- description: Cursor to retrieve the next page of results (only present if hasMore is true)
- totalCount:
- type: integer
- description: Total number of customers matching the criteria (excluding pagination)
- Error405:
+ description: Hosted URL the customer should be sent to in order to complete verification. The URL is single-use and expires at `expiresAt`. To generate a new link (for example, after the previous one expires or is abandoned), call this endpoint again.
+ example: https://kyc.lightspark.com/onboard/abc123def456
+ expiresAt:
+ type: string
+ format: date-time
+ description: Time at which the hosted link expires and can no longer be used.
+ example: '2027-01-15T14:32:00Z'
+ provider:
+ $ref: '#/components/schemas/KycProvider'
+ token:
+ type: string
+ description: Provider-specific token that can be used in place of the hosted URL — for example, to embed the provider's SDK directly in your application. Only returned for providers that support direct SDK integration. Whether to use the hosted URL or the embedded SDK is up to you; both flows result in the same `kycStatus` update on the customer.
+ example: _act-sbx-jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+ ContactVerificationConfirmRequest:
type: object
+ description: Request body for confirming an email or phone verification challenge.
required:
- - message
- - status
- code
properties:
- status:
- type: integer
- enum:
- - 405
- description: HTTP status code
code:
type: string
- description: |
- | Error Code | Description |
- |------------|-------------|
- | METHOD_NOT_ALLOWED | The HTTP method is not supported for this endpoint |
- enum:
- - METHOD_NOT_ALLOWED
- message:
- type: string
- description: Error message
- details:
- type: object
- description: Additional error details
- additionalProperties: true
- CustomerCreateRequest:
+ description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`.
+ example: '123456'
+ ScaFactor:
+ type: string
+ enum:
+ - SMS_OTP
+ - TOTP
+ - PASSKEY
+ description: |
+ A Strong Customer Authentication factor.
+
+ | Factor | Description |
+ |--------|-------------|
+ | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. |
+ | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). |
+ | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. |
+ ScaFactorView:
type: object
+ description: An enrolled Strong Customer Authentication factor.
required:
- - customerType
+ - factor
properties:
- platformCustomerId:
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The kind of enrolled factor.
+ credentialId:
type: string
- description: Platform-specific customer identifier. If not provided, one will be generated by the system.
- example: 9f84e0c2a72c4fa
- customerType:
- $ref: '#/components/schemas/CustomerType'
- region:
+ description: The per-credential id, populated only for `PASSKEY` factors (the id passed to delete a passkey). Omitted for `TOTP` and `SMS_OTP`, which have no per-credential id.
+ name:
type: string
- description: Country code (ISO 3166-1 alpha-2) representing the customer's regional identity. This determines the regulatory jurisdiction and KYC requirements for the customer. Required if the customer will use currencies with different KYC requirements across regions. A customer with accounts in multiple regions should be registered as separate customers. This field is immutable after creation.
- example: US
- currencies:
+ description: An optional human-readable label for this factor.
+ ScaFactorList:
+ type: object
+ description: The Strong Customer Authentication factors a customer has enrolled.
+ required:
+ - factors
+ properties:
+ factors:
type: array
+ description: The customer's enrolled SCA factors.
items:
- type: string
- description: List of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Required if the customer will use more than one sending currency, since the correct currencies cannot always be inferred. If not provided, currencies will be inferred from the customer's region. Some currency combinations may require separate customers — if so, the request will be rejected with details.
- example:
- - USD
- - USDC
- email:
- type: string
- format: email
- description: Email address for the customer.
- example: john.doe@example.com
- phoneNumber:
- type: string
- pattern: ^\+[1-9]\d{1,14}$
- description: Phone number for the customer in strict E.164 format.
- example: '+14155551234'
- umaAddress:
+ $ref: '#/components/schemas/ScaFactorView'
+ TotpFactorEnrollRequest:
+ type: object
+ title: TOTP Factor Enroll Request
+ description: Start enrolling a time-based one-time-password (TOTP) authenticator factor.
+ required:
+ - type
+ properties:
+ type:
type: string
- description: Optional UMA address identifier. If not provided during customer creation, one will be generated by the system. If provided during customer update, the UMA address will be updated to the provided value. This is an optional identifier to route payments to the customer. This is an optional identifier to route payments to the customer.
- example: $john.doe@uma.domain.com
- IndividualCustomerCreateRequest:
- title: Individual Customer Create Request
- allOf:
- - $ref: '#/components/schemas/CustomerCreateRequest'
- - $ref: '#/components/schemas/IndividualCustomerFields'
- - type: object
- properties:
- idType:
- $ref: '#/components/schemas/IdentificationType'
- identifier:
- type: string
- writeOnly: true
- description: The individual's tax identification number. Required to onboard the individual as a US account holder. Only SSN and ITIN are currently accepted for an individual account holder; other identification types are rejected. Write-only — never returned in customer responses.
- example: 123-45-6789
- BusinessInfo:
+ enum:
+ - TOTP
+ description: Discriminator selecting the TOTP factor. TOTP enrollment needs no other input at start.
+ PasskeyFactorEnrollRequest:
type: object
- description: Additional information required for business entities
+ title: Passkey Factor Enroll Request
+ description: Start enrolling a WebAuthn passkey factor.
required:
- - legalName
- - taxId
- - incorporatedOn
+ - type
properties:
- legalName:
+ type:
type: string
- description: Legal name of the business
- example: Acme Corporation, Inc.
- doingBusinessAs:
+ enum:
+ - PASSKEY
+ description: Discriminator selecting the passkey factor. Passkey enrollment needs no other input at start.
+ ScaFactorEnrollRequestOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/TotpFactorEnrollRequest'
+ - $ref: '#/components/schemas/PasskeyFactorEnrollRequest'
+ discriminator:
+ propertyName: type
+ mapping:
+ TOTP: '#/components/schemas/TotpFactorEnrollRequest'
+ PASSKEY: '#/components/schemas/PasskeyFactorEnrollRequest'
+ description: Which SCA factor to begin enrolling, selected by `type`. `SMS_OTP` is not enrollable (it uses the customer's verified phone), so only `TOTP` and `PASSKEY` are valid here.
+ TotpEnrollmentStart:
+ type: object
+ description: The shared secret a customer's authenticator app needs to enroll a TOTP factor. Returned by `POST /sca/factors` for a `TOTP` request; the customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with the first code their app produces.
+ required:
+ - type
+ - secret
+ - secretBase32Encoded
+ - totpUri
+ properties:
+ type:
type: string
- description: Trade name or DBA name of the business, if different from the legal name
- example: Acme
- country:
+ enum:
+ - TOTP
+ description: Discriminator identifying this as the TOTP enrollment-start payload.
+ secret:
type: string
- description: Country of incorporation or registration (ISO 3166-1 alpha-2)
- example: US
- registrationNumber:
+ description: The raw TOTP shared secret.
+ secretBase32Encoded:
type: string
- description: Business registration number
- example: '5523041'
- incorporatedOn:
+ description: The Base32-encoded shared secret, suitable for manual entry into an authenticator app that does not scan QR codes.
+ totpUri:
type: string
- format: date
- description: Date of incorporation in ISO 8601 format (YYYY-MM-DD)
- example: '2018-03-14'
- entityType:
- $ref: '#/components/schemas/EntityType'
- taxId:
+ description: The `otpauth://` provisioning URI (the QR-code payload) the customer's authenticator app scans to enroll the factor.
+ example: otpauth://totp/Grid:customer@example.com?secret=ABC123&issuer=Grid
+ PasskeyEnrollmentStart:
+ type: object
+ description: Opaque WebAuthn registration options relayed to the end user's device to enroll a passkey factor. Grid performs no crypto; pass `options` to the device's WebAuthn API to produce a credential, then submit that credential to the confirm endpoint unmodified.
+ required:
+ - type
+ - options
+ - allowedOrigins
+ - relyingPartyId
+ properties:
+ type:
type: string
- description: Tax identification number
- example: 47-1234567
- countriesOfOperation:
+ enum:
+ - PASSKEY
+ description: Discriminator identifying this as the passkey enrollment-start payload.
+ options:
+ type: object
+ additionalProperties: true
+ description: Opaque WebAuthn `PublicKeyCredentialCreationOptions`. Pass to the device's WebAuthn registration API unmodified.
+ allowedOrigins:
type: array
+ description: The origins the WebAuthn registration ceremony may run against. The origin the credential is produced against must be one of these and must be echoed back on the confirm call.
items:
type: string
- description: List of countries where the business operates (ISO 3166-1 alpha-2)
example:
- - US
- businessType:
- $ref: '#/components/schemas/BusinessType'
- purposeOfAccount:
- $ref: '#/components/schemas/PurposeOfAccount'
- sourceOfFunds:
+ - https://app.example.com
+ relyingPartyId:
type: string
- description: The primary source of funds for the business
- example: Funds derived from customer payments for software services
- expectedMonthlyTransactionCount:
+ description: The WebAuthn relying-party id the credential is bound to.
+ example: app.example.com
+ ScaFactorEnrollStartOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/TotpEnrollmentStart'
+ - $ref: '#/components/schemas/PasskeyEnrollmentStart'
+ discriminator:
+ propertyName: type
+ mapping:
+ TOTP: '#/components/schemas/TotpEnrollmentStart'
+ PASSKEY: '#/components/schemas/PasskeyEnrollmentStart'
+ description: 'The factor-specific material needed to complete enrollment, keyed by `type`: a TOTP shared secret + provisioning URI, or the WebAuthn registration options for a passkey.'
+ TotpEnrollmentConfirmRequest:
+ type: object
+ description: The shared secret returned by the TOTP enrollment start, plus the first code the customer's authenticator app produces, submitted to confirm and finalize the TOTP factor.
+ required:
+ - type
+ - secret
+ - code
+ properties:
+ type:
type: string
enum:
- - COUNT_UNDER_10
- - COUNT_10_TO_100
- - COUNT_100_TO_500
- - COUNT_500_TO_1000
- - COUNT_OVER_1000
- description: Expected number of transactions per month
- example: COUNT_100_TO_500
- expectedMonthlyTransactionVolume:
+ - TOTP
+ description: Discriminator selecting the TOTP confirm variant.
+ secret:
type: string
- enum:
- - VOLUME_UNDER_10K
- - VOLUME_10K_TO_100K
- - VOLUME_100K_TO_1M
- - VOLUME_1M_TO_10M
- - VOLUME_OVER_10M
- description: Expected total transaction volume per month in USD equivalent
- example: VOLUME_100K_TO_1M
- expectedRecipientJurisdictions:
- type: array
- items:
- type: string
- description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2)
- example:
- - US
- naicsCode:
+ description: The shared secret returned as `secret` by the TOTP enrollment start, threaded back to bind the confirmation to that enrollment.
+ code:
type: string
- pattern: ^\d{2,6}$
- description: NAICS code describing the nature of the business (2-6 digits)
- example: '541511'
- sourceOfFundsCategories:
- type: array
- items:
- $ref: '#/components/schemas/SourceOfFundsCategory'
- description: Structured source-of-funds categories for the business
- example:
- - OPERATING_REVENUE
- sourceOfFundsOtherDescription:
+ description: The current time-based one-time code from the customer's authenticator app. In sandbox, the code is always `123456`.
+ example: '123456'
+ PasskeyEnrollmentConfirmRequest:
+ type: object
+ description: The WebAuthn credential a device produced for a passkey registration challenge, submitted to enroll the passkey factor.
+ required:
+ - type
+ - origin
+ - credential
+ properties:
+ type:
type: string
- minLength: 1
- maxLength: 500
- description: Description of the source of funds when OTHER is selected
- example: Proceeds from a legal settlement
- purposeOfAccountOtherDescription:
+ enum:
+ - PASSKEY
+ description: Discriminator selecting the passkey confirm variant.
+ origin:
+ type: string
+ description: The WebAuthn origin the `credential` was produced against (one of the enrollment start's `allowedOrigins`).
+ example: https://app.example.com
+ credential:
+ type: object
+ additionalProperties: true
+ description: Opaque WebAuthn credential the device produced from the enrollment start's `options`.
+ ScaFactorConfirmRequestOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/TotpEnrollmentConfirmRequest'
+ - $ref: '#/components/schemas/PasskeyEnrollmentConfirmRequest'
+ discriminator:
+ propertyName: type
+ mapping:
+ TOTP: '#/components/schemas/TotpEnrollmentConfirmRequest'
+ PASSKEY: '#/components/schemas/PasskeyEnrollmentConfirmRequest'
+ description: 'The proof that finalizes enrollment, keyed by `type`: the TOTP shared secret + code, or the passkey `origin` + `credential`.'
+ TotpEnrollmentConfirmResponse:
+ type: object
+ description: The one-time recovery codes issued once a TOTP factor is enrolled. These are shown to the customer only once; store them somewhere safe to recover access if the authenticator device is lost.
+ required:
+ - type
+ - recoveryCodes
+ properties:
+ type:
type: string
- minLength: 1
- maxLength: 500
- description: Description of the account purpose when OTHER is selected
- example: Escrow for equipment leases
- expectedCounterpartyCountries:
+ enum:
+ - TOTP
+ description: Discriminator identifying this as the TOTP enrollment result.
+ recoveryCodes:
type: array
+ description: The one-time recovery codes for this TOTP factor.
items:
type: string
- description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
example:
- - US
- BusinessCustomerCreateRequest:
- title: Business Customer Create Request
- allOf:
- - $ref: '#/components/schemas/CustomerCreateRequest'
- - $ref: '#/components/schemas/BusinessCustomerFields'
- - type: object
- properties:
- businessInfo:
- $ref: '#/components/schemas/BusinessInfo'
- CustomerCreateRequestOneOf:
+ - ABCD-EFGH-IJKL
+ - MNOP-QRST-UVWX
+ PasskeyEnrollmentConfirmResponse:
+ type: object
+ description: The enrolled passkey factor returned after a successful confirmation.
+ required:
+ - type
+ - factor
+ properties:
+ type:
+ type: string
+ enum:
+ - PASSKEY
+ description: Discriminator identifying this as the passkey enrollment result.
+ factor:
+ $ref: '#/components/schemas/ScaFactorView'
+ ScaFactorConfirmResponseOneOf:
oneOf:
- - $ref: '#/components/schemas/IndividualCustomerCreateRequest'
- - $ref: '#/components/schemas/BusinessCustomerCreateRequest'
+ - $ref: '#/components/schemas/TotpEnrollmentConfirmResponse'
+ - $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse'
discriminator:
- propertyName: customerType
+ propertyName: type
mapping:
- INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest'
- BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest'
- Error409:
+ TOTP: '#/components/schemas/TotpEnrollmentConfirmResponse'
+ PASSKEY: '#/components/schemas/PasskeyEnrollmentConfirmResponse'
+ description: The enrollment result, keyed by `type`.
+ ScaLoginStartRequest:
type: object
+ description: Selects which enrolled factor to start an SCA login with. The factor must already be enrolled (or, for `SMS_OTP`, the phone verified).
required:
- - message
- - status
- - code
+ - factor
properties:
- status:
- type: integer
- enum:
- - 409
- description: HTTP status code
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor to authenticate with.
+ ScaLoginStart:
+ type: object
+ description: 'The factor-specific material a customer needs to complete an SCA login. Each factor surfaces only the fields it issues: `SMS_OTP` carries `challengeId` and `expiresAt`; `TOTP` carries neither (the customer reads the code from their authenticator app); `PASSKEY` carries the opaque WebAuthn `passkeyOptions` with `allowedOrigins` and `relyingPartyId`.'
+ required:
+ - factor
+ properties:
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor this login was started for.
+ challengeId:
+ type:
+ - string
+ - 'null'
+ description: The challenge handle for an `SMS_OTP` login, threaded back on the complete call. Present only for `SMS_OTP`.
+ expiresAt:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: Absolute UTC timestamp after which the `SMS_OTP` code expires. Present only for `SMS_OTP`.
+ example: '2025-10-03T12:05:00Z'
+ passkeyOptions:
+ type:
+ - object
+ - 'null'
+ additionalProperties: true
+ description: Opaque WebAuthn assertion request options. Present only for `PASSKEY`; pass to the device's WebAuthn API to produce the assertion submitted on the complete call.
+ allowedOrigins:
+ type:
+ - array
+ - 'null'
+ items:
+ type: string
+ description: The origins the WebAuthn ceremony may run against. Present only for `PASSKEY`.
+ example:
+ - https://app.example.com
+ relyingPartyId:
+ type:
+ - string
+ - 'null'
+ description: The WebAuthn relying-party id. Present only for `PASSKEY`.
+ example: app.example.com
+ ScaLoginCompleteRequest:
+ type: object
+ description: Completes an SCA login by submitting the proof for the started factor. Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`), plus the `factor` being completed and, for `SMS_OTP`, the `challengeId` returned by the login start.
+ required:
+ - factor
+ anyOf:
+ - required:
+ - code
+ - required:
+ - passkeyAssertion
+ - origin
+ properties:
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor being completed; must match the started login.
+ challengeId:
+ type:
+ - string
+ - 'null'
+ description: The challenge handle returned by the login start, required for `SMS_OTP` and omitted for other factors.
code:
- type: string
- description: |
- | Error Code | Description |
- |------------|-------------|
- | TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL | Transaction is not pending platform approval |
- | UMA_ADDRESS_EXISTS | UMA address already exists |
- | EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential |
- | EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued |
- | CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform |
- enum:
- - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL
- - UMA_ADDRESS_EXISTS
- - EMAIL_OTP_EMAIL_ALREADY_EXISTS
- - EMAIL_OTP_CREDENTIAL_SET_CHANGED
- - CONFLICT
- message:
- type: string
- description: Error message
- details:
- type: object
- description: Additional error details
+ type:
+ - string
+ - 'null'
+ description: The one-time code the customer received by SMS, or read from their authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is always `123456`.
+ example: '123456'
+ passkeyAssertion:
+ type:
+ - object
+ - 'null'
additionalProperties: true
- Error404:
+ description: Opaque WebAuthn assertion produced by the device from the login start's `passkeyOptions`. Required when completing a `PASSKEY` login.
+ origin:
+ type:
+ - string
+ - 'null'
+ description: The WebAuthn origin the `passkeyAssertion` was produced against (one of the login start's `allowedOrigins`). Required alongside `passkeyAssertion`; omit it for the `code` path.
+ example: https://app.example.com
+ ScaLoginComplete:
type: object
+ description: The status of a completed SCA login session.
required:
- - message
- status
- - code
properties:
status:
- type: integer
- enum:
- - 404
- description: HTTP status code
- code:
type: string
- description: |
- | Error Code | Description |
- |------------|-------------|
- | TRANSACTION_NOT_FOUND | Transaction not found |
- | INVITATION_NOT_FOUND | Invitation not found |
- | USER_NOT_FOUND | Customer not found |
- | QUOTE_NOT_FOUND | Quote not found |
- | LOOKUP_REQUEST_NOT_FOUND | Lookup request not found |
- | TOKEN_NOT_FOUND | Token not found |
- | BULK_UPLOAD_JOB_NOT_FOUND | Bulk upload job not found |
- | REFERENCE_NOT_FOUND | Reference not found |
- | UMA_NOT_FOUND | The UMA address is well-formed but no receiver exists at the counterparty VASP |
- | STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND | Stablecoin provider account link not found |
- enum:
- - TRANSACTION_NOT_FOUND
- - INVITATION_NOT_FOUND
- - USER_NOT_FOUND
- - QUOTE_NOT_FOUND
- - LOOKUP_REQUEST_NOT_FOUND
- - TOKEN_NOT_FOUND
- - BULK_UPLOAD_JOB_NOT_FOUND
- - REFERENCE_NOT_FOUND
- - UMA_NOT_FOUND
- - STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND
- message:
- type: string
- description: Error message
- details:
- type: object
- description: Additional error details
- additionalProperties: true
- Error410:
+ description: The status of the login session, passed through verbatim (Grid does not normalize it). A successful login reports `SUCCESS`; other values indicate the login did not complete and should be surfaced to the caller.
+ example: SUCCESS
+ Error423:
type: object
required:
- message
@@ -10793,97 +12217,117 @@ components:
status:
type: integer
enum:
- - 410
+ - 423
description: HTTP status code
code:
type: string
description: |
| Error Code | Description |
|------------|-------------|
- | CUSTOMER_DELETED | Customer has been permanently deleted |
+ | ACCOUNT_LOCKED | The customer's login is temporarily locked (or suspended) after too many failed attempts. Inspect `details.lockedUntil` for when the customer may retry, and `details.failedAttempts` for the current count. |
enum:
- - CUSTOMER_DELETED
+ - ACCOUNT_LOCKED
message:
type: string
description: Error message
details:
type: object
- description: Additional error details
+ description: Lockout detail. `lockedUntil` is the UTC timestamp the customer may retry after (absent when suspended indefinitely); `failedAttempts` is the current cumulative failed-login count; `suspended` is true when the account is locked with no automatic expiry.
+ properties:
+ suspended:
+ type: boolean
+ lockedUntil:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ failedAttempts:
+ type: integer
additionalProperties: true
- CustomerUpdateRequest:
- title: Customer Update Request
- description: Request body for `PATCH /customers/{customerId}`. When `email` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer email and every tied `EMAIL_OTP` credential through the endpoint's signed-retry flow. When `phoneNumber` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer phone number and every tied `SMS_OTP` credential through the same signed-retry flow. Update `email` and `phoneNumber` in separate PATCH calls.
+ RecordSecurityEventRequest:
type: object
+ description: Records a client-side security event for the customer so Grid can maintain the customer's login-security state (SCA session revocation and failed-login lockout).
required:
- - customerType
+ - eventType
properties:
- customerType:
- $ref: '#/components/schemas/CustomerType'
- currencies:
- type: array
- items:
- type: string
- description: Updated list of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Replaces the existing list. Some currency combinations may require separate customers — if so, the request will be rejected with details.
- example:
- - USD
- - EUR
- - USDC
- email:
- type: string
- format: email
- description: Email address for the customer. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `EMAIL_OTP` credential across all tied Embedded Wallets.
- example: john.doe@example.com
- phoneNumber:
+ eventType:
type: string
- pattern: ^\+[1-9]\d{1,14}$
- description: Phone number for the customer in strict E.164 format. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `SMS_OTP` credential across all tied Embedded Wallets. Send phone number and email updates as separate PATCH calls.
- example: '+14155551234'
- umaAddress:
- type: string
- description: Optional UMA address identifier. If provided, the customer's UMA address will be updated. This is an optional identifier to route payments to the customer.
- example: $john.doe@uma.domain.com
- IndividualCustomerUpdateRequest:
- title: Individual Customer Update Request
- allOf:
- - $ref: '#/components/schemas/CustomerUpdateRequest'
- - $ref: '#/components/schemas/IndividualCustomerFields'
- BusinessCustomerUpdateRequest:
- title: Business Customer Update Request
- allOf:
- - $ref: '#/components/schemas/CustomerUpdateRequest'
- - $ref: '#/components/schemas/BusinessCustomerFields'
- CustomerUpdateRequestOneOf:
- oneOf:
- - $ref: '#/components/schemas/IndividualCustomerUpdateRequest'
- - $ref: '#/components/schemas/BusinessCustomerUpdateRequest'
- discriminator:
- propertyName: customerType
- mapping:
- INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest'
- BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest'
- SignedRequestChallenge:
- title: Signed Request Challenge
+ enum:
+ - RESET_PASSWORD_COMPLETED
+ - FAILED_LOGIN_ATTEMPT
+ description: |
+ The security event to record:
+
+ | Value | Effect |
+ |-------|--------|
+ | `RESET_PASSWORD_COMPLETED` | Revokes every active SCA session for the customer and clears the failed-login counter. |
+ | `FAILED_LOGIN_ATTEMPT` | Increments the cumulative failed-login counter and escalates the lockout at each milestone: 5 attempts → 15 minutes, 6 → 30 minutes, 7 → 1 hour, 8 → 24 hours, 9 or more → suspension. |
+ example: FAILED_LOGIN_ATTEMPT
+ RecordSecurityEventResponse:
type: object
+ description: The customer's login-security state after recording the event, so the integrator can surface a lockout to the end user.
required:
- - payloadToSign
- - requestId
- - expiresAt
- description: Common base for two-step signed-retry challenge responses on Embedded Wallet endpoints (credential registration or revocation, session refresh or revocation, wallet export, customer email updates, and similar). Holds the signing fields shared across every challenge shape; each variant composes this base via `allOf` and adds its own resource `id` (and `type`, when applicable) with variant-specific description and example.
+ - eventType
+ - suspended
+ - failedAttempts
properties:
- payloadToSign:
+ eventType:
type: string
- description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request.
- example: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_EXAMPLE"}'
- requestId:
+ enum:
+ - RESET_PASSWORD_COMPLETED
+ - FAILED_LOGIN_ATTEMPT
+ description: The event that was recorded.
+ example: FAILED_LOGIN_ATTEMPT
+ suspended:
+ type: boolean
+ description: Whether the customer's login is currently suspended (locked with no automatic expiry). A suspended customer must go through a password reset (`RESET_PASSWORD_COMPLETED`) to clear the lockout.
+ lockedUntil:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: When the customer may attempt to log in again, if temporarily locked. Null when not locked, or when `suspended` is true (no automatic expiry).
+ example: '2025-10-03T12:15:00Z'
+ failedAttempts:
+ type: integer
+ description: The customer's current cumulative failed-login count.
+ example: 5
+ TwoFactorResetStartRequest:
+ type: object
+ description: Selects which enrolled factor to reset via the liveness-gated recovery flow.
+ required:
+ - factor
+ properties:
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The enrolled factor to reset.
+ TwoFactorResetStart:
+ type: object
+ description: The reset handle plus the opaque liveness handles a caller relays to the end-user device to complete the liveness check. `resetId` threads the ceremony together (status and complete reference it). `livenessAccessToken` and `verificationLink` are omitted when they are not returned.
+ required:
+ - resetId
+ properties:
+ resetId:
type: string
- description: Grid-issued `Request:` identifier for this pending request. Echo this value exactly in the `Request-Id` header on the signed retry so the server can correlate the retry with the issued challenge.
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ description: Identifier for this reset; pass it to the status and complete endpoints.
+ livenessAccessToken:
+ type:
+ - string
+ - 'null'
+ description: Access token for the embedded liveness/verification SDK, bound to this reset. Omitted when one is not returned.
+ verificationLink:
+ type:
+ - string
+ - 'null'
+ description: Hosted identity-verification page URL for completing liveness. Omitted when one is not returned.
expiresAt:
- type: string
+ type:
+ - string
+ - 'null'
format: date-time
- description: Timestamp after which this challenge is no longer valid. The signed retry must be submitted before this time.
- example: '2026-04-08T15:35:00Z'
- Error424:
+ description: Absolute UTC timestamp at the end of the reset window. Omitted when one is not returned.
+ example: '2025-10-03T12:30:00Z'
+ Error429:
type: object
required:
- message
@@ -10893,24 +12337,16 @@ components:
status:
type: integer
enum:
- - 424
+ - 429
description: HTTP status code
code:
type: string
description: |
| Error Code | Description |
|------------|-------------|
- | PAYREQ_REQUEST_FAILED | Payment request failed |
- | COUNTERPARTY_PUBKEY_FETCH_ERROR | Error fetching counterparty public key |
- | NO_COMPATIBLE_UMA_VERSION | No compatible UMA version |
- | LNURLP_REQUEST_FAILED | LNURLP request failed |
- | EMAIL_OTP_CREDENTIAL_SYNC_FAILED | Failed to update one or more tied EMAIL_OTP credentials |
+ | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header |
enum:
- - PAYREQ_REQUEST_FAILED
- - COUNTERPARTY_PUBKEY_FETCH_ERROR
- - NO_COMPATIBLE_UMA_VERSION
- - LNURLP_REQUEST_FAILED
- - EMAIL_OTP_CREDENTIAL_SYNC_FAILED
+ - RATE_LIMITED
message:
type: string
description: Error message
@@ -10918,54 +12354,175 @@ components:
type: object
description: Additional error details
additionalProperties: true
- KycLinkCreateRequest:
+ TwoFactorResetStatus:
type: object
- description: Request body for generating a hosted KYC link for an existing customer.
+ description: The status of an in-progress 2FA reset, polled until it reaches a terminal value.
+ required:
+ - status
+ - factor
+ - expiresAt
properties:
- redirectUri:
+ status:
type: string
- format: uri
- description: URI the customer is redirected to after completing the hosted KYC flow. Must start with `https://` (or `http://` for local development). Embedded in the returned `kycUrl`.
- example: https://app.example.com/onboarding/completed
- KycProvider:
- type: string
- description: The KYC provider that will perform identity verification for the customer. Grid selects the provider based on the customer's region and platform configuration; the value is informational for platforms that want to integrate directly with the provider's SDK.
- enum:
- - SUMSUB
- example: SUMSUB
- KycLinkResponse:
+ enum:
+ - INITIATED
+ - PENDING_REVIEW
+ - LIVENESS_PASSED
+ - COMPLETED
+ - REJECTED
+ - EXPIRED
+ description: |-
+ The reset status.
+ | Value | Terminal | Meaning | |-------|----------|---------| | `INITIATED` | no | Reset started; liveness not yet submitted. Keep polling. | | `PENDING_REVIEW` | no | Liveness submitted; under review. Keep polling. | | `LIVENESS_PASSED` | no | Liveness passed; call the complete endpoint to finish the reset. | | `COMPLETED` | **yes** | Reset finished; the lost factor is cleared and re-enrollment can begin. | | `REJECTED` | **yes** | Liveness failed. Stop polling; start a new reset. | | `EXPIRED` | **yes** | The reset window closed before it completed. Stop polling; start a new reset. |
+ Stop polling on any terminal value.
+ example: INITIATED
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor being reset.
+ enrollmentStatus:
+ type:
+ - string
+ - 'null'
+ enum:
+ - PENDING
+ - COMPLETED
+ - null
+ description: The enrollment status of the replacement factor. `PENDING` until the customer finishes re-enrolling, then `COMPLETED`. Null for an `SMS_OTP` reset, where completing the reset enrolls the new number directly with no separate enrollment step.
+ example: PENDING
+ expiresAt:
+ type: string
+ format: date-time
+ description: When the reset window closes. Poll no longer than this.
+ example: '2025-10-03T12:15:00Z'
+ completedAt:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: When the reset completed; null until then.
+ example: null
+ TwoFactorResetCompleteRequest:
type: object
- description: A hosted KYC link that the customer can complete to verify their identity.
+ description: Optional body for completing a 2FA reset. Only needed when resetting the `SMS_OTP` factor to a new phone number; omit the body entirely otherwise.
+ properties:
+ mobile:
+ type: object
+ description: For an `SMS_OTP` reset, the new mobile number to enroll as the customer completes the reset. Required for an `SMS_OTP` reset; ignored for `TOTP` / `PASSKEY` resets.
+ required:
+ - countryCode
+ - number
+ properties:
+ countryCode:
+ type: string
+ description: The country dialing code, including the leading `+`.
+ example: '+1'
+ number:
+ type: string
+ description: The national subscriber number, without the country code.
+ example: '4155550123'
+ ScaChallenge:
+ type: object
+ description: |-
+ A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed.
+
+ When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`.
+
+ **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged.
required:
- - kycUrl
+ - id
- expiresAt
- - provider
+ - factor
+ - availableFactors
properties:
- kycUrl:
+ id:
type: string
- description: Hosted URL the customer should be sent to in order to complete verification. The URL is single-use and expires at `expiresAt`. To generate a new link (for example, after the previous one expires or is abandoned), call this endpoint again.
- example: https://kyc.lightspark.com/onboard/abc123def456
+ description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation).
+ example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007
expiresAt:
type: string
format: date-time
- description: Time at which the hosted link expires and can no longer be used.
- example: '2027-01-15T14:32:00Z'
- provider:
- $ref: '#/components/schemas/KycProvider'
- token:
- type: string
- description: Provider-specific token that can be used in place of the hosted URL — for example, to embed the provider's SDK directly in your application. Only returned for providers that support direct SDK integration. Whether to use the hosted URL or the embedded SDK is up to you; both flows result in the same `kycStatus` update on the customer.
- example: _act-sbx-jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- ContactVerificationConfirmRequest:
+ description: Absolute UTC timestamp after which this challenge can no longer be authorized.
+ example: '2025-10-03T12:05:00Z'
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor this challenge was issued for. Defaults to `SMS_OTP`.
+ availableFactors:
+ type: array
+ description: The factors the customer may use to satisfy this challenge.
+ items:
+ $ref: '#/components/schemas/ScaFactor'
+ example:
+ - SMS_OTP
+ purpose:
+ type:
+ - string
+ - 'null'
+ description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow).
+ example: PAYOUT
+ passkeyAssertionOptions:
+ type:
+ - object
+ - 'null'
+ additionalProperties: true
+ description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`.
+ passkeyAllowedOrigins:
+ type:
+ - array
+ - 'null'
+ items:
+ type: string
+ description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case.
+ example:
+ - https://app.example.com
+ BeneficiaryTrustStart:
type: object
- description: Request body for confirming an email or phone verification challenge.
- required:
- - code
+ description: The SCA challenge (if any) a caller authorizes to finish trusting (or untrusting) a beneficiary. The beneficiary is identified by its `externalAccountId`, so the confirm call needs no separate handle. `scaChallenge` is omitted when no challenge is issued; the caller then confirms without a `challengeId`.
+ properties:
+ scaChallenge:
+ $ref: '#/components/schemas/ScaChallenge'
+ description: The SCA challenge to satisfy on the confirm call. Omitted when no challenge is issued.
+ BeneficiaryTrustConfirmRequest:
+ type: object
+ description: Confirms trusting or untrusting a beneficiary by submitting the SCA proof. Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`) and, when the start issued one, the `challengeId`. The beneficiary is identified by the `externalAccountId` in the path — no separate handle is needed.
+ anyOf:
+ - required:
+ - code
+ - required:
+ - passkeyAssertion
+ - origin
properties:
+ challengeId:
+ type:
+ - string
+ - 'null'
+ description: The challenge handle from the trust start's `scaChallenge`, when one was issued. Omit when the start returned no challenge.
code:
- type: string
- description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`.
+ type:
+ - string
+ - 'null'
+ description: The one-time code the customer received by SMS, or read from their authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is always `123456`.
example: '123456'
+ passkeyAssertion:
+ type:
+ - object
+ - 'null'
+ additionalProperties: true
+ description: Opaque WebAuthn assertion produced by the device from the challenge's assertion options. Required when satisfying a `PASSKEY` challenge.
+ origin:
+ type:
+ - string
+ - 'null'
+ description: The WebAuthn origin the `passkeyAssertion` was produced against. Required alongside `passkeyAssertion`; omit it for the `code` path.
+ example: https://app.example.com
+ BeneficiaryTrustConfirm:
+ type: object
+ description: The result of a confirm-trust / confirm-untrust call. `trusted` is `true` after a successful trust and `false` after a successful untrust.
+ required:
+ - trusted
+ properties:
+ trusted:
+ type: boolean
+ description: Whether the beneficiary is now trusted. `true` after a successful trust, `false` after a successful untrust.
InternalAccountType:
title: Internal Account Type
type: string
@@ -18202,20 +19759,6 @@ components:
description: The payment rail to use for the transfer. Must be one of the rails supported by the destination account. If not specified, the system will select a default rail.
allOf:
- $ref: '#/components/schemas/PaymentRail'
- ScaFactor:
- type: string
- enum:
- - SMS_OTP
- - TOTP
- - PASSKEY
- description: |
- A Strong Customer Authentication factor.
-
- | Factor | Description |
- |--------|-------------|
- | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. |
- | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). |
- | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. |
TransferOutRequest:
type: object
required:
@@ -18238,9 +19781,6 @@ components:
maxLength: 80
description: 'Free-form information about the payment that travels with it to the recipient. The field this populates depends on the payment rail: for ACH it populates the Addenda record, for FedNow and RTP it populates the remittanceInformation field, and for wires it populates the OBI (Originator to Beneficiary Information) / beneficiary information.'
example: '12345'
- scaFactor:
- $ref: '#/components/schemas/ScaFactor'
- description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`.
CurrencyPreference:
type: object
required:
@@ -18475,60 +20015,6 @@ components:
mapping:
ACCOUNT: '#/components/schemas/AccountDestination'
UMA_ADDRESS: '#/components/schemas/UmaAddressDestination'
- ScaChallenge:
- type: object
- description: |-
- A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed.
-
- When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`.
-
- **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged.
- required:
- - id
- - expiresAt
- - factor
- - availableFactors
- properties:
- id:
- type: string
- description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation).
- example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007
- expiresAt:
- type: string
- format: date-time
- description: Absolute UTC timestamp after which this challenge can no longer be authorized.
- example: '2025-10-03T12:05:00Z'
- factor:
- $ref: '#/components/schemas/ScaFactor'
- description: The factor this challenge was issued for. Defaults to `SMS_OTP`.
- availableFactors:
- type: array
- description: The factors the customer may use to satisfy this challenge.
- items:
- $ref: '#/components/schemas/ScaFactor'
- example:
- - SMS_OTP
- purpose:
- type:
- - string
- - 'null'
- description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow).
- example: PAYOUT
- passkeyAssertionOptions:
- type:
- - object
- - 'null'
- additionalProperties: true
- description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`.
- passkeyAllowedOrigins:
- type:
- - array
- - 'null'
- items:
- type: string
- description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case.
- example:
- - https://app.example.com
Quote:
type: object
required:
@@ -18753,33 +20239,6 @@ components:
- 'null'
description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`.
example: https://app.example.com
- Error429:
- type: object
- required:
- - message
- - status
- - code
- properties:
- status:
- type: integer
- enum:
- - 429
- description: HTTP status code
- code:
- type: string
- description: |
- | Error Code | Description |
- |------------|-------------|
- | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header |
- enum:
- - RATE_LIMITED
- message:
- type: string
- description: Error message
- details:
- type: object
- description: Additional error details
- additionalProperties: true
TransactionListResponse:
type: object
required:
diff --git a/mintlify/payouts-and-b2b/payment-flow/send-payment.mdx b/mintlify/payouts-and-b2b/payment-flow/send-payment.mdx
index 548095f84..689b824f8 100644
--- a/mintlify/payouts-and-b2b/payment-flow/send-payment.mdx
+++ b/mintlify/payouts-and-b2b/payment-flow/send-payment.mdx
@@ -362,6 +362,15 @@ If a transaction fails, Grid initiates a refund automatically. You'll receive `O
| `FAILED` | Transfer failed — refund initiated automatically (track via `refund` object) |
| `EXPIRED` | Quote expired without execution |
+## Strong Customer Authentication (EU customers)
+
+Customers in SCA-regulated regions (in practice the EU: EUR / USDC) must confirm
+payments with Strong Customer Authentication. When it applies, the payment comes
+back `PENDING_AUTHORIZATION` with an `scaChallenge` that you authorize before the
+transfer is released; for every other customer nothing changes. See
+[Per-transaction authorization](/platform-overview/sca/per-transaction-authorization)
+for the full walkthrough.
+
## Checking Payment Status
Configure a webhook endpoint to receive real-time notifications when payment status changes:
diff --git a/mintlify/platform-overview/sca/factor-enrollment.mdx b/mintlify/platform-overview/sca/factor-enrollment.mdx
new file mode 100644
index 000000000..a890e0395
--- /dev/null
+++ b/mintlify/platform-overview/sca/factor-enrollment.mdx
@@ -0,0 +1,145 @@
+---
+icon: "/images/icons/key2.svg"
+title: "Factor enrollment"
+description: "Enroll and manage a customer's TOTP and passkey factors"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+Applies only to customers in an SCA-required region (EU). Every endpoint here
+returns **`409`** for other customers.
+
+
+`SMS_OTP` needs no enrollment; a code is sent to the customer's verified phone.
+`TOTP` and `PASSKEY` must be enrolled before a customer can authenticate with
+them. Enrolled factors then appear in `scaChallenge.availableFactors` and can be
+requested per transaction (see
+[per-transaction authorization](/platform-overview/sca/per-transaction-authorization)).
+
+Enrollment is two calls, both discriminated by a `type` field (`TOTP` or
+`PASSKEY`) — the same shape the [login and session](/platform-overview/sca/login-and-sessions)
+endpoints use:
+
+- `POST /sca/factors` — start enrollment; returns the factor-specific material.
+- `POST /sca/factors/confirm` — finish enrollment with the factor-specific proof.
+
+All paths below are relative to `https://api.lightspark.com/grid/2025-10-13`.
+
+## Enroll a TOTP authenticator
+
+
+
+
+```bash
+POST /sca/factors?customerId={customerId}
+
+{ "type": "TOTP" }
+```
+
+Returns the shared secret and an `otpauth://` provisioning URI. Render `totpUri`
+as a QR code (or show `secretBase32Encoded` for manual entry) so the customer can
+add it to their authenticator app.
+
+```json
+{
+ "type": "TOTP",
+ "secret": "…",
+ "secretBase32Encoded": "ABC123…",
+ "totpUri": "otpauth://totp/Grid:customer@example.com?secret=ABC123&issuer=Grid"
+}
+```
+
+
+
+Submit the `secret` from the start call plus the first code the app produces.
+Grid returns one-time **recovery codes**; surface them to the customer once and
+don't store them server-side.
+
+```bash
+POST /sca/factors/confirm?customerId={customerId}
+
+{ "type": "TOTP", "secret": "…", "code": "123456" }
+```
+
+```json
+{ "type": "TOTP", "recoveryCodes": ["ABCD-EFGH-IJKL", "MNOP-QRST-UVWX"] }
+```
+
+A wrong or expired code returns `400`. **In sandbox, the code is always
+`123456`.**
+
+
+
+## Enroll a passkey
+
+Passkey enrollment is a standard WebAuthn registration ceremony. Grid issues the
+options, the customer's device produces the credential, and you hand it back.
+
+
+A customer may have **only one passkey**. If one is already enrolled, starting
+another returns `409` (`PASSKEY_ALREADY_ENROLLED`) — delete the existing passkey
+first (see below). `GET /sca/factors` therefore lists at most one passkey.
+
+
+
+
+
+```bash
+POST /sca/factors?customerId={customerId}
+
+{ "type": "PASSKEY" }
+```
+
+```json
+{
+ "type": "PASSKEY",
+ "options": { "…": "opaque WebAuthn PublicKeyCredentialCreationOptions" },
+ "allowedOrigins": ["https://app.example.com"],
+ "relyingPartyId": "app.example.com"
+}
+```
+
+Pass `options` unmodified to the device's WebAuthn API
+(`navigator.credentials.create`). The ceremony must run against one of
+`allowedOrigins`.
+
+
+Submit the credential the device produced and the `origin` it ran against.
+
+```bash
+POST /sca/factors/confirm?customerId={customerId}
+
+{ "type": "PASSKEY", "origin": "https://app.example.com", "credential": { "…": "opaque WebAuthn credential" } }
+```
+
+Returns the enrolled `factor` (an `ScaFactorView`, including the `credentialId`
+you'll use to delete it later). An invalid credential or origin returns `400`.
+
+
+
+## List enrolled factors
+
+```bash
+GET /sca/factors?customerId={customerId}
+```
+
+```json
+{
+ "factors": [
+ { "factor": "TOTP", "name": "Authenticator app" },
+ { "factor": "PASSKEY", "credentialId": "…", "name": "iPhone" }
+ ]
+}
+```
+
+`credentialId` is populated only for `PASSKEY` factors.
+
+## Delete a factor
+
+```bash
+DELETE /sca/factors/{credentialId}?customerId={customerId}
+```
+
+Returns `204`. Use the `credentialId` from the factor list (or the confirm
+response). Today only passkeys carry a `credentialId`, so this is how you remove
+an enrolled passkey.
diff --git a/mintlify/platform-overview/sca/login-and-sessions.mdx b/mintlify/platform-overview/sca/login-and-sessions.mdx
new file mode 100644
index 000000000..65e466ec1
--- /dev/null
+++ b/mintlify/platform-overview/sca/login-and-sessions.mdx
@@ -0,0 +1,123 @@
+---
+icon: "/images/icons/shield.svg"
+title: "Login & sessions"
+description: "The end-user SCA login and the session it grants"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+Applies only to customers in an SCA-required region (EU). Every endpoint here
+returns **`409`** for other customers.
+
+
+Per-transaction authorization gates individual debits. The **SCA login** is
+separate: it authenticates the end user to open a longer-lived session that
+covers reads and account access beyond the per-transaction window. Grid provides
+the login plumbing; your application decides when to drive it (for example, when
+a customer opens their account and the previous session has lapsed).
+
+
+**A customer's EUR / USDC accounts aren't provisioned until their first SCA
+login after KYC approval.** Provisioning is deferred from KYC-approval time to
+the first login that opens a valid SCA session, so a freshly KYC-approved
+customer's EUR / USDC accounts won't appear in `GET /customers/internal-accounts`
+until then. Expect those accounts to be unavailable, and drive the SCA login
+once KYC is approved, before relying on them.
+
+
+All paths below are relative to `https://api.lightspark.com/grid/2025-10-13`.
+
+## Logging in
+
+
+
+
+```bash
+POST /sca/login/start?customerId={customerId}
+
+{ "factor": "SMS_OTP" }
+```
+
+The response carries only what the chosen factor needs:
+
+- **`SMS_OTP`**: a code is dispatched; you get back `challengeId` and `expiresAt`.
+- **`TOTP`**: nothing extra; the customer reads the code from their app.
+- **`PASSKEY`**: WebAuthn `passkeyOptions` (with `allowedOrigins` and `relyingPartyId`) to pass to the device.
+
+The factor must already be enrolled (or, for `SMS_OTP`, the phone verified). See
+[factor enrollment](/platform-overview/sca/factor-enrollment).
+
+
+Submit the proof for the factor you started with: `code` for `SMS_OTP` / `TOTP`
+(echoing `challengeId` for `SMS_OTP`), or `passkeyAssertion` + `origin` for
+`PASSKEY`.
+
+```bash
+POST /sca/login/complete?customerId={customerId}
+
+{ "factor": "SMS_OTP", "challengeId": "…", "code": "123456" }
+```
+
+```json
+{ "status": "SUCCESS" }
+```
+
+A `status` of `SUCCESS` means the session is open for 180 days and revokes any
+previous SCA session for that customer. Any other value means the
+login did not complete; the field is passed through verbatim, so treat only
+`SUCCESS` as success. An invalid or expired proof returns `400`. **In sandbox,
+the code is always `123456`.**
+
+
+
+## Session scope and fresh authentication
+
+An active SCA login session covers EUR / USDC account reads for 180 days. A
+request for transaction history older than 90 days requires fresh SCA, even when
+the broader session has not expired. When Grid indicates that a session is
+missing, expired, or too old for the requested history, restart the login flow
+before retrying the read.
+
+## Account-security signals
+
+Grid runs an adaptive-authentication risk engine that maintains each customer's
+login-security state. Because your application owns the customer's login, you
+report the security-relevant events it sees so the engine can act on them:
+
+```bash
+POST /sca/record-event?customerId={customerId}
+
+{ "eventType": "FAILED_LOGIN_ATTEMPT" }
+```
+
+Returns the customer's resulting login-security state so you can surface a
+lockout — `{ eventType, suspended, lockedUntil, failedAttempts }`. When the
+customer is locked out, this (and `POST /sca/login/complete`) returns `423` with
+`details.lockedUntil` (when they may retry) and `details.failedAttempts`.
+`eventType` must be one of:
+
+| `eventType` | Effect |
+|---|---|
+| `FAILED_LOGIN_ATTEMPT` | Increments the failed-login counter and escalates a lockout: **5 → 15 min, 6 → 30 min, 7 → 1 hour, 8 → 24 hours, 9 or more → suspension.** |
+| `RESET_PASSWORD_COMPLETED` | Revokes every active SCA session for the customer and clears the failed-login counter. |
+
+Report `FAILED_LOGIN_ATTEMPT` on each failed sign-in and `RESET_PASSWORD_COMPLETED`
+once a password recovery finishes. Any other value returns `400`.
+
+
+The failed-login counter is cumulative and is **not** reset by a successful
+login; only `RESET_PASSWORD_COMPLETED` clears it. Record that event after a
+password recovery to zero the counter and clear a time-bounded lockout, rather
+than relying on the customer simply logging in again. A suspended customer
+(9 or more failed attempts) requires support intervention; password recovery
+does not unsuspend the account.
+
+
+## Your responsibilities
+
+Grid provides the SCA endpoints and risk decisions; your application owns the
+end-user login and session experience. Report every failed sign-in and completed
+password recovery through `record-event`, enforce any returned lockout before
+offering another login attempt, and do not store or reuse a customer's TOTP
+secret or passkey material. Treat TOTP secrets and WebAuthn ceremony data as
+end-user credentials, not platform credentials.
diff --git a/mintlify/platform-overview/sca/overview.mdx b/mintlify/platform-overview/sca/overview.mdx
new file mode 100644
index 000000000..76654d56a
--- /dev/null
+++ b/mintlify/platform-overview/sca/overview.mdx
@@ -0,0 +1,135 @@
+---
+icon: "/images/icons/shield.svg"
+title: "Strong Customer Authentication"
+description: "How Grid satisfies PSD2 SCA for EU customers"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+**This applies only to customers in a region where Strong Customer Authentication
+is required, in practice customers in the EU (EUR / USDC).** For every other
+customer none of this appears: money-movement calls complete as usual, no
+`scaChallenge` is returned, the authentication endpoints return `409`, and you
+can skip this section.
+
+
+Under PSD2, EU e-money and e-money-token (EUR / USDC) money movement must be
+confirmed by the end user with Strong Customer Authentication (SCA). Grid wraps
+SCA so you satisfy it through the same resources you already use. There is no
+separate product to integrate, and the same request shapes work for every
+customer whether or not SCA applies.
+
+This section covers the whole surface:
+
+
+
+ Authorize a money movement that came back `PENDING_AUTHORIZATION`. This is the flow you hit most often.
+
+
+ Enroll and manage a customer's TOTP and passkey factors.
+
+
+ The end-user SCA login and the session it grants for reads.
+
+
+ Whitelist a payee once so future sends to it skip the per-transaction ceremony.
+
+
+ Recover a customer who has lost their factors, gated by an identity (liveness) check.
+
+
+
+## What SCA covers
+
+SCA gates **debits** on EU-regulated balances. EUR / USDC reads are covered by
+an active [SCA login session](/platform-overview/sca/login-and-sessions), while
+non-EUR/USDC accounts do not require SCA. "Dynamic linking" means the authorization is cryptographically
+bound to the transaction's amount and payee (PSD2 Article 97(2)); it forces a
+fresh, transaction-specific challenge, and it's the reason some flows can't use
+TOTP (see [Authentication factors](#authentication-factors)).
+
+| Operation | SCA required? | Dynamically linked? |
+|---|---|---|
+| Send EUR / USDC (SEPA + intra-ledger) | Yes | Yes |
+| Convert **from** EUR / USDC (the swap leg) | Yes | Yes |
+| On-chain / Lightning withdrawal | Yes | No |
+| Trust / untrust a beneficiary | Yes | No |
+| Send to an **already-trusted** beneficiary | Lighter, no dynamic linking | No |
+| Reading balances / history | Covered by the login session | n/a |
+| Non-EUR/USDC accounts (e.g. USD) | No | n/a |
+
+## Authentication factors
+
+The `scaChallenge.availableFactors` field tells you which factors a customer may
+use; `scaChallenge.factor` is the one in use (default `SMS_OTP`).
+
+| Factor | Enrollment | Per-transaction debit |
+|--------|-----------|-----------------------|
+| `SMS_OTP` | None; a code is sent to the customer's verified phone | ✅ Default |
+| `PASSKEY` | Required (WebAuthn credential) | ✅ |
+| `TOTP` | Required (authenticator app) | Available only where the challenge is not dynamically linked; never assume it is available for a particular debit |
+
+TOTP is barred from dynamically-linked debits because an authenticator code is
+derived only from a clock and a shared secret, so it can't be bound to *this*
+amount and payee. It stays valid for flows that don't require dynamic linking:
+login, trusting a beneficiary, sends to an already-trusted beneficiary, and
+eligible on-chain or Lightning withdrawals. Always use the factors in
+`scaChallenge.availableFactors`; that field, not the operation name, is the
+source of truth for the challenge Grid issued.
+
+## The authorization flow
+
+For an SCA-required customer, a money-movement call that would otherwise complete
+instead returns the resource in status **`PENDING_AUTHORIZATION`** carrying an
+**`scaChallenge`**, and the transfer is not released until the challenge is
+satisfied. A single money movement can require **more than one** challenge in
+sequence, so loop on status rather than assuming one authorization releases the
+transfer.
+
+```mermaid
+sequenceDiagram
+ participant P as Your platform
+ participant G as Grid
+ participant U as End user
+
+ P->>G: Initiate money movement (execute a quote)
+ G-->>P: PENDING_AUTHORIZATION + scaChallenge
+ G->>U: Deliver challenge (e.g. SMS OTP)
+ loop while status == PENDING_AUTHORIZATION
+ U-->>P: Provide proof (code / passkey assertion)
+ P->>G: POST .../authorize (scaChallenge.id + proof)
+ G-->>P: Updated resource + next scaChallenge (if any)
+ end
+ G-->>P: Resource leaves PENDING_AUTHORIZATION, transfer released
+```
+
+[Per-transaction authorization](/platform-overview/sca/per-transaction-authorization)
+covers the mechanics: authorizing the quote, the multi-step loop, resending an
+expired code, and the realtime-funding-quote nuance.
+
+## Lifetimes & limits
+
+| Aspect | Behavior |
+|---|---|
+| Challenge expiry | Each challenge carries an absolute `scaChallenge.expiresAt` (UTC). SMS codes expire after 5 minutes; TOTP and passkey challenges expire after 10 minutes. After expiry, the challenge can no longer be authorized. |
+| Resend | `SMS_OTP` only. Resending reuses the existing challenge and does not extend `expiresAt`; `PASSKEY` (and `TOTP`) codes can't be resent. |
+| Repeated failures | A challenge permits at most 5 attempts. Too many failed authorizations may invalidate it and return `429 RATE_LIMITED`, so honor `Retry-After`. |
+| Login session | A completed [SCA login](/platform-overview/sca/login-and-sessions) grants a 180-day session. EUR / USDC history older than 90 days requires fresh SCA even during that session. |
+| Account lockout | Repeated `FAILED_LOGIN_ATTEMPT` signals escalate a lockout (5 → 15 min, 6 → 30 min, 7 → 1 hour, 8 → 24 hours, 9+ → suspension). See [account-security signals](/platform-overview/sca/login-and-sessions#account-security-signals). |
+| 2FA reset window | A started reset carries its own `expiresAt`; complete it before then. |
+
+## Errors you'll encounter
+
+| Status | Meaning | What to do |
+|---|---|---|
+| `400` | Invalid or expired proof: wrong code, expired challenge, or a factor that can't satisfy this challenge (e.g. `TOTP` on a dynamically-linked debit). | Re-collect the proof. If the code lapsed, resend (`SMS_OTP`) or start over. |
+| `409` | SCA isn't required for this customer (non-EU), there's no pending challenge, or the factor's code can't be resent (e.g. `PASSKEY`). | Don't retry the same call. Treat a non-EU `409` as nothing to authorize. |
+| `429` | `RATE_LIMITED`: too many attempts or resends, and the challenge may now be invalidated. | Honor `Retry-After`; you may need to restart the flow. |
+| `404` | The customer, transaction, quote, external account, or reset wasn't found. | Check the id. |
+
+## Calling a customer outside SCA-regulated regions
+
+Every authentication endpoint returns **`409`** for customers outside
+SCA-regulated regions (non-EU), and no `scaChallenge` is ever attached to their
+transactions. You don't need to branch on region. Handle `scaChallenge` when it's
+present and treat its absence as nothing to do.
diff --git a/mintlify/platform-overview/sca/per-transaction-authorization.mdx b/mintlify/platform-overview/sca/per-transaction-authorization.mdx
new file mode 100644
index 000000000..ec55da5ff
--- /dev/null
+++ b/mintlify/platform-overview/sca/per-transaction-authorization.mdx
@@ -0,0 +1,75 @@
+---
+icon: "/images/icons/lock.svg"
+title: "Per-transaction authorization"
+description: "Authorize an SCA-gated money movement"
+"og:image": "/images/og/og-get-started.png"
+---
+
+This is the flow you hit most often: an SCA-required customer initiates a money
+movement, it comes back `PENDING_AUTHORIZATION` with an `scaChallenge`, and you
+authorize it before the transfer is released. For where this sits in the wider
+SCA surface, see the [overview](/platform-overview/sca/overview).
+
+import StrongCustomerAuthentication from '/snippets/sca/strong-customer-authentication.mdx';
+
+
+
+## Walkthrough by flow
+
+The mechanics above are the same everywhere: inspect for an `scaChallenge`,
+submit an `ScaAuthorization`, and repeat until the resource leaves
+`PENDING_AUTHORIZATION`. What differs between flows is *which* call first returns
+the challenge and *which* resource you authorize. Here is each one end to end.
+
+
+
+The common case — lock a quote, execute it, authorize the quote.
+
+
+
+`POST /quotes` returns a quote as usual. A standard (prefunded) send carries no
+challenge at quote time.
+
+
+`POST /quotes/{quoteId}/execute` returns the **quote** in `PENDING_AUTHORIZATION`
+with an `scaChallenge`.
+
+
+`POST /quotes/{quoteId}/authorize` with the proof. Re-inspect the returned quote:
+if it remains `PENDING_AUTHORIZATION`, authorize its next `scaChallenge`. Do not
+assume a fixed number of challenges for a cross-currency or other multi-step
+send.
+
+
+
+
+
+Here the challenge is issued at *quote* time and `paymentInstructions` are
+withheld until you clear it — you authorize before you fund.
+
+
+
+`POST /quotes` for a realtime-funded send returns `202` /
+`PENDING_AUTHORIZATION` with an `scaChallenge`. `paymentInstructions` are
+**omitted** from this response.
+
+
+`POST /quotes/{quoteId}/authorize` with the proof — the challenge is carried by
+the quote. Re-inspect the returned quote and continue the authorization loop if
+it remains `PENDING_AUTHORIZATION`.
+
+
+Read `paymentInstructions` from the returned (advanced) quote and fund the
+transfer. Reading them off the initial pending response would show the customer
+nothing to fund.
+
+
+
+
+
+`transfer-out` has no associated quote, and per-transaction SCA is authorized
+only on the quote resource, so SCA-gated EU debits are **not** offered on this
+endpoint — use the quote + `execute` flow above for EU customers. For customers
+outside SCA-regulated regions, `transfer-out` proceeds as usual.
+
+
diff --git a/mintlify/platform-overview/sca/trusted-beneficiaries.mdx b/mintlify/platform-overview/sca/trusted-beneficiaries.mdx
new file mode 100644
index 000000000..0e59b32aa
--- /dev/null
+++ b/mintlify/platform-overview/sca/trusted-beneficiaries.mdx
@@ -0,0 +1,94 @@
+---
+icon: "/images/icons/checkmark1.svg"
+title: "Trusted beneficiaries"
+description: "Whitelist a payee so future sends skip the per-transaction ceremony"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+Applies only to customers in an SCA-required region (EU). Every endpoint here
+returns **`409`** for other customers.
+
+
+Trusting a beneficiary is a one-time, SCA-gated step that whitelists an external
+account. Only **USDC addresses** can be trusted today. Once trusted, future sends
+to that payee are no longer dynamically linked; they drop to a lighter
+authentication instead of a full per-transaction challenge. Use it for recurring
+payouts to known destinations.
+
+The beneficiary is identified end-to-end by its **`externalAccountId`** in the
+path, so there is no separate whitelist handle to track. All paths below are
+relative to `https://api.lightspark.com/grid/2025-10-13`.
+
+## Trusting a beneficiary
+
+
+
+```bash
+POST /customers/external-accounts/{externalAccountId}/trust
+```
+
+Returns the `scaChallenge` to satisfy:
+
+```json
+{ "scaChallenge": { "id": "…", "factor": "SMS_OTP", "expiresAt": "2025-10-03T12:05:00Z", "availableFactors": ["SMS_OTP"] } }
+```
+
+
+`scaChallenge` may be **omitted** when no challenge is issued. In that case,
+confirm directly without a `challengeId`.
+
+
+
+Submit the proof for the factor Grid returned in `scaChallenge`: `code` for
+`SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`. Echo
+`challengeId` when start issued one. Do not assume a factor is available unless
+it appears in `scaChallenge.availableFactors`.
+
+```bash
+POST /customers/external-accounts/{externalAccountId}/trust/confirm
+
+{ "challengeId": "…", "code": "123456" }
+```
+
+```json
+{ "trusted": true }
+```
+
+An invalid or expired proof returns `400`. **In sandbox, the code is always
+`123456`.**
+
+
+
+## Untrusting a beneficiary
+
+Untrusting mirrors trusting: a start call issues the challenge, then confirm
+submits the proof.
+
+
+
+
+```bash
+POST /customers/external-accounts/{externalAccountId}/untrust
+```
+
+Returns the `scaChallenge` to satisfy, omitted when no challenge is issued (the
+caller then confirms without a `challengeId`).
+
+
+
+Submit the proof for the factor Grid returned in `scaChallenge`: `code` for
+`SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`. Echo
+`challengeId` when start issued one. Returns `trusted: false`.
+
+```bash
+POST /customers/external-accounts/{externalAccountId}/untrust/confirm
+
+{ "challengeId": "…", "code": "123456" }
+```
+
+
+
+
+Once untrusted, sends to that beneficiary are dynamically linked again and each
+one requires a full per-transaction challenge.
diff --git a/mintlify/platform-overview/sca/two-factor-reset.mdx b/mintlify/platform-overview/sca/two-factor-reset.mdx
new file mode 100644
index 000000000..0a7be1861
--- /dev/null
+++ b/mintlify/platform-overview/sca/two-factor-reset.mdx
@@ -0,0 +1,82 @@
+---
+icon: "/images/icons/key2.svg"
+title: "Two-factor reset"
+description: "Recover a customer who has lost their SCA factor"
+"og:image": "/images/og/og-get-started.png"
+---
+
+
+Applies only to customers in an SCA-required region (EU). Every endpoint here
+returns **`409`** for other customers.
+
+
+When a customer loses an enrolled factor (a new phone, a deleted authenticator),
+they recover it with a **2FA reset**: an identity (liveness) check that, once
+passed, clears the lost factor so they can re-enroll it. It's a poll-based flow:
+start, poll until liveness passes, then complete.
+
+All paths below are relative to `https://api.lightspark.com/grid/2025-10-13`.
+
+
+
+
+```bash
+POST /sca/factors/reset?customerId={customerId}
+
+{ "factor": "TOTP" }
+```
+
+Returns **`201`** with a `resetId` and opaque liveness handles. Embed
+`livenessAccessToken` in the verification SDK, or send the customer to
+`verificationLink`. `expiresAt` bounds the reset window. Reset initiation is
+rate-limited to **5 per 24 hours** per customer; beyond that this returns `429`.
+
+```json
+{
+ "resetId": "…",
+ "livenessAccessToken": "…",
+ "verificationLink": "https://…",
+ "expiresAt": "2025-10-03T12:30:00Z"
+}
+```
+
+
+
+```bash
+GET /sca/factors/reset/{resetId}?customerId={customerId}
+```
+
+```json
+{ "status": "INITIATED" }
+```
+
+Poll with a short backoff. `status` is one of `INITIATED`, `PENDING_REVIEW`,
+`LIVENESS_PASSED`, `COMPLETED`, `REJECTED`, or `EXPIRED`:
+
+- `INITIATED` — reset started, liveness not yet submitted; keep polling.
+- `PENDING_REVIEW` — liveness submitted and under review; keep polling.
+- `LIVENESS_PASSED` — proceed to complete.
+- `COMPLETED` (reset finished, factor cleared), `REJECTED` (liveness failed), and
+ `EXPIRED` (window closed) are **terminal** — stop polling. On `REJECTED` or
+ `EXPIRED`, start a new reset.
+
+The response also carries `factor`, `enrollmentStatus` (`PENDING` until the
+replacement factor is re-enrolled, then `COMPLETED`; `null` for an `SMS_OTP`
+reset), `expiresAt` (the window bound), and `completedAt`. Stop at any terminal
+status or once `expiresAt` passes; never poll indefinitely.
+
+
+
+```bash
+POST /sca/factors/reset/{resetId}/complete?customerId={customerId}
+
+{ "mobile": { "countryCode": "+1", "number": "4155550123" } }
+```
+
+Returns `204` and clears the lost factor. For an `SMS_OTP` reset, include the new
+`mobile` number in the body — it's enrolled as the customer completes the reset;
+other factors need no body. Calling it before liveness has passed returns `400`.
+The customer can then re-enroll via
+[factor enrollment](/platform-overview/sca/factor-enrollment).
+
+
diff --git a/mintlify/ramps/conversion-flows/fiat-crypto-conversion.mdx b/mintlify/ramps/conversion-flows/fiat-crypto-conversion.mdx
index d4a4c56b0..fd3b8ed79 100644
--- a/mintlify/ramps/conversion-flows/fiat-crypto-conversion.mdx
+++ b/mintlify/ramps/conversion-flows/fiat-crypto-conversion.mdx
@@ -112,6 +112,14 @@ curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
```javascript
function displayPaymentInstructions(quote) {
+ // EU customers only: an SCA-gated realtime-funding quote comes back
+ // PENDING_AUTHORIZATION with paymentInstructions withheld until its
+ // scaChallenge is authorized (see "Strong Customer Authentication" below).
+ // Authorize first, then display — don't read paymentInstructions while pending.
+ if (!quote.paymentInstructions?.length) {
+ return { pendingAuthorization: true, scaChallenge: quote.scaChallenge };
+ }
+
const instructions = quote.paymentInstructions[0];
return {
@@ -233,6 +241,16 @@ curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
`immediatelyExecute` can only be used with sources that are either internal accounts or external accounts with direct pull functionality (e.g., ACH pull).
+## Strong Customer Authentication (EU customers)
+
+Customers in SCA-regulated regions (in practice the EU: EUR / USDC) must confirm
+money movements with Strong Customer Authentication, and a conversion out of
+EUR / USDC is gated on its swap leg. When it applies, the transaction comes back
+`PENDING_AUTHORIZATION` with an `scaChallenge` that you authorize before the
+conversion is released; for every other customer nothing changes. See
+[Per-transaction authorization](/platform-overview/sca/per-transaction-authorization)
+for the full walkthrough.
+
## Best practices
diff --git a/mintlify/snippets/sca/strong-customer-authentication.mdx b/mintlify/snippets/sca/strong-customer-authentication.mdx
new file mode 100644
index 000000000..1b0e3ef16
--- /dev/null
+++ b/mintlify/snippets/sca/strong-customer-authentication.mdx
@@ -0,0 +1,106 @@
+
+**This applies only to customers in a region where Strong Customer Authentication
+is required, in practice customers in the EU (EUR / USDC).** For every other
+customer, none of this appears: money-movement calls complete as usual, no
+`scaChallenge` is returned, and the authorization endpoints are not used. If you
+don't serve EU customers you can skip this section.
+
+
+Under PSD2, EU e-money and e-money-token (EUR / USDC) money movement must be
+confirmed by the end user with Strong Customer Authentication (SCA). Grid wraps
+SCA so you satisfy it through the same resources you already use. There is no
+separate SCA product to integrate.
+
+### When you'll encounter it
+
+For an SCA-required customer, a money-movement call that would otherwise complete
+instead returns the transaction (or quote) in status **`PENDING_AUTHORIZATION`**
+with an **`scaChallenge`** object, and the transfer is **not** released until the
+challenge is satisfied. This affects debits such as:
+
+- Sending EUR / USDC (SEPA and intra-ledger transfers)
+- Cross-currency conversions from EUR / USDC (the swap leg)
+- On-chain and Lightning withdrawals
+
+EUR / USDC reads are covered by an active SCA login session; non-EUR/USDC
+accounts do not require SCA.
+
+### Authentication factors
+
+The `scaChallenge.availableFactors` field tells you which factors the customer
+may use. `scaChallenge.factor` is the one in use (default `SMS_OTP`).
+
+| Factor | Enrollment | Per-transaction debit |
+|--------|-----------|-----------------------|
+| `SMS_OTP` | None; a code is sent to the customer's verified phone | ✅ Default |
+| `PASSKEY` | Required (WebAuthn credential) | ✅ |
+| `TOTP` | Required (authenticator app) | Available only where the challenge is not dynamically linked |
+
+TOTP cannot satisfy a dynamically linked debit because its code cannot be bound
+to the amount and payee. For a non-dynamically-linked challenge, use TOTP only
+when it appears in `scaChallenge.availableFactors`; that field is authoritative.
+
+Request a specific factor per transaction with the optional top-level `scaFactor`
+field on `execute` (`SMS_OTP` default, or `PASSKEY`).
+
+### Satisfying a challenge
+
+Submit an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize` for the
+quote that carries the challenge. Provide exactly one of `code` (for `SMS_OTP`)
+or `passkeyAssertion` + `origin` (for `PASSKEY`):
+
+```bash
+curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/authorize \
+ -u "$GRID_API_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{ "code": "123456" }'
+```
+
+
+**Write your client to loop on status, not on a fixed challenge count.** Treat
+`scaChallenge` as the challenge to satisfy *now*, not necessarily the only one:
+after authorizing, re-inspect the returned quote, and if it is still
+`PENDING_AUTHORIZATION` it carries the **next** `scaChallenge` (a new `id`) —
+authorize that one too and repeat until it leaves `PENDING_AUTHORIZATION`.
+
+
+Once the quote is in `PENDING_AUTHORIZATION`, authorize it:
+`POST /quotes/{quoteId}/authorize`. This is the single authorize path for both
+`execute` (pre-funded) and realtime-funding quotes. The challenge — and the SMS
+code or passkey assertion that satisfies it — only exists after the challenge is
+issued, so the proof is always supplied on this follow-up call, never on the
+originating request.
+
+
+For a **realtime-funding quote**, the `202` / `PENDING_AUTHORIZATION` response
+**withholds `paymentInstructions`** until the challenge is authorized. Authorize
+first, then read `paymentInstructions` from the returned (advanced) quote. If you
+read them off the initial pending response you'll show the customer nothing to
+fund.
+
+
+If an SMS code lapses before it's used, re-send it. The existing challenge is
+reused, and its `expiresAt` is not extended. Use the quote resend endpoint:
+`POST /quotes/{quoteId}/authorize/resend`.
+
+```bash
+curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/{quoteId}/authorize/resend \
+ -u "$GRID_API_TOKEN"
+```
+
+
+In **sandbox**, the SMS code is always `123456`.
+
+
+### Reducing prompts for repeat payees
+
+Trusting a beneficiary (a one-time SCA-gated whitelisting step) lets subsequent
+sends to that payee skip the per-transaction challenge. Use this for recurring
+payouts to known destinations rather than authorizing every send.
+
+### Calling a customer outside SCA-regulated regions
+
+The authorization endpoints return **`409`** for customers outside SCA-regulated
+regions (non-EU), and no `scaChallenge` is ever attached to their transactions.
+You don't need to branch on region. Handle `scaChallenge` when it's present and
+treat its absence as nothing to do.
diff --git a/openapi.yaml b/openapi.yaml
index 1e57d7e52..40e821c1a 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -1127,180 +1127,97 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/internal-accounts:
+ /sca/factors:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer whose factors are listed or enrolled.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
get:
- summary: List Customer internal accounts
+ summary: List enrolled SCA factors
description: |
- Retrieve a list of internal accounts with optional filtering parameters. Returns all
- internal accounts that match the specified filters. If no filters are provided, returns all internal accounts
- (paginated).
+ List the Strong Customer Authentication factors the customer has enrolled.
- Internal accounts are created automatically when a customer is created based on the platform configuration.
- operationId: listCustomerInternalAccounts
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: listScaFactors
tags:
- - Internal Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: currency
- in: query
- description: Filter by currency code
- required: false
- schema:
- type: string
- - name: customerId
- in: query
- description: Filter by internal accounts associated with a specific customer
- required: false
- schema:
- type: string
- - name: type
- in: query
- description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts.
- required: false
- schema:
- $ref: '#/components/schemas/InternalAccountType'
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
responses:
'200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/InternalAccountListResponse'
- '400':
- description: Bad request - Invalid parameters
+ description: The customer's enrolled SCA factors.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/ScaFactorList'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /platform/internal-accounts:
- get:
- summary: List platform internal accounts
- description: |
- Retrieve a list of all internal accounts that belong to the platform, as opposed to an individual customer.
-
- These accounts are created automatically when the platform is configured for each supported currency. They can be used for things like distributing bitcoin rewards to customers, or for other platform-wide purposes.
- operationId: listPlatformInternalAccounts
- tags:
- - Internal Accounts
- security:
- - BasicAuth: []
- parameters:
- - name: currency
- in: query
- description: Filter by currency code
- required: false
- schema:
- type: string
- - name: type
- in: query
- description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts.
- required: false
- schema:
- $ref: '#/components/schemas/InternalAccountType'
- responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/PlatformInternalAccountListResponse'
- '400':
- description: Bad request - Invalid parameters
+ '404':
+ description: Customer not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/external-accounts:
- get:
- summary: List Customer external accounts
+ post:
+ summary: Start SCA factor enrollment
description: |
- Retrieve a list of external accounts with optional filtering parameters. Returns all
- external accounts that match the specified filters. If no filters are provided, returns all external accounts
- (paginated).
+ Begin enrolling an SCA factor for the customer. Enrollment covers the
+ explicit, opt-in factors a customer chooses to add — the request body's
+ `type` selects `TOTP` or `PASSKEY`. Returns the factor-specific material
+ needed to finish via `POST /sca/factors/confirm`.
- External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform.
- operationId: listCustomerExternalAccounts
+ `SMS_OTP` is implicit and is not enrolled through this endpoint. Every
+ customer in an SCA-regulated region has a verified phone number from
+ customer creation (via the Contact Verification flows —
+ `POST /customers/{customerId}/verify-phone` and `.../verify-phone/confirm`),
+ so SMS is always available as a factor with no extra setup and appears
+ among the customer's enrolled factors in `GET /sca/factors`.
+
+ A customer may have **only one passkey**. Starting a passkey enrollment when
+ one is already enrolled returns `409` (`PASSKEY_ALREADY_ENROLLED`) — delete it
+ via `DELETE /sca/factors/{credentialId}` first.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startScaFactorEnrollment
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: currency
- in: query
- description: Filter by currency code
- required: false
- schema:
- type: string
- - name: customerId
- in: query
- description: Filter by external accounts associated with a specific customer
- required: false
- schema:
- type: string
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ScaFactorEnrollRequestOneOf'
responses:
'200':
- description: Successful operation
+ description: Enrollment started; the factor-specific completion material is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccountListResponse'
+ $ref: '#/components/schemas/ScaFactorEnrollStartOneOf'
'400':
- description: Bad request - Invalid parameters
+ description: Invalid request
content:
application/json:
schema:
@@ -1311,18 +1228,52 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer (`CONFLICT`), or a passkey enrollment was requested while one is already enrolled (`PASSKEY_ALREADY_ENROLLED`) — only one passkey per customer is supported.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /sca/factors/confirm:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer confirming a factor enrollment.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
post:
- summary: Add a new external account
- description: Register a new external bank account for a customer.
- operationId: createCustomerExternalAccount
+ summary: Confirm SCA factor enrollment
+ description: |
+ Finalize the factor enrollment started by `POST /sca/factors`. The request
+ body is discriminated by `type`: for `TOTP`, submit the shared `secret` from
+ the start call plus the first `code`; for `PASSKEY`, submit the WebAuthn
+ `credential` the device produced plus the `origin` it was produced against.
+ The threaded secret/credential binds the confirmation to its enrollment, so
+ no separate id is needed.
+
+ A TOTP confirmation returns one-time recovery codes (shown once); a passkey
+ confirmation returns the enrolled factor.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the TOTP code is always `123456`.
+ operationId: confirmScaFactorEnrollment
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
requestBody:
@@ -1330,48 +1281,16 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccountCreateRequest'
- examples:
- usBankAccount:
- summary: Create external US bank account
- value:
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- currency: USD
- accountInfo:
- accountType: USD_ACCOUNT
- accountNumber: '12345678901'
- routingNumber: '123456789'
- bankAccountType: CHECKING
- bankName: Chase Bank
- platformAccountId: ext_acc_123456
- beneficiary:
- beneficiaryType: INDIVIDUAL
- fullName: John Doe
- birthDate: '1990-01-15'
- nationality: US
- address:
- line1: 123 Main Street
- city: San Francisco
- state: CA
- postalCode: '94105'
- country: US
- sparkWallet:
- summary: Create external Spark wallet
- value:
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- currency: BTC
- accountInfo:
- accountType: SPARK_WALLET
- address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
+ $ref: '#/components/schemas/ScaFactorConfirmRequestOneOf'
responses:
- '201':
- description: External account created successfully
+ '200':
+ description: Factor enrolled; the factor-specific result is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccount'
+ $ref: '#/components/schemas/ScaFactorConfirmResponseOneOf'
'400':
- description: Bad request
+ description: Invalid or incorrect confirmation proof
content:
application/json:
schema:
@@ -1382,8 +1301,14 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'409':
- description: Conflict - External account already exists
+ description: SCA is not required for this customer.
content:
application/json:
schema:
@@ -1394,29 +1319,36 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/external-accounts/{externalAccountId}:
+ /sca/factors/{credentialId}:
parameters:
- - name: externalAccountId
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer whose factor is being deleted.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ - name: credentialId
in: path
- description: System-generated unique external account identifier
+ description: The credential id of the enrolled factor to delete (from the factor's `credentialId`).
required: true
schema:
type: string
- get:
- summary: Get customer external account by ID
- description: Retrieve a customer external account by its system-generated ID
- operationId: getCustomerExternalAccountById
+ delete:
+ summary: Delete an enrolled SCA factor
+ description: |
+ Delete an enrolled SCA factor by its credential id. Today only `PASSKEY`
+ factors carry a `credentialId` and are deletable this way.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: deleteScaFactor
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ExternalAccount'
+ '204':
+ description: Factor deleted; no content is returned.
'401':
description: Unauthorized
content:
@@ -1424,89 +1356,64 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: External account not found
+ description: Customer or factor not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
+ '409':
+ description: SCA is not required for this customer.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- delete:
- summary: Delete customer external account by ID
- description: Delete a customer external account by its system-generated ID
- operationId: deleteCustomerExternalAccountById
- tags:
- - External Accounts
- security:
- - BasicAuth: []
- responses:
- '204':
- description: External account deleted successfully
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: External account not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /platform/external-accounts:
- get:
- summary: List platform external accounts
+ /sca/login/start:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer starting an SCA login.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ post:
+ summary: Start an SCA login
description: |
- Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer.
+ Begin an SCA login for the customer with the chosen factor, opening the
+ end-user SCA session (an exemption gating read / account access beyond the
+ per-transaction window). Returns factor-specific material: `SMS_OTP`
+ dispatches a code and returns a `challengeId` + `expiresAt`; `TOTP` returns
+ only the factor (the customer reads the code from their app); `PASSKEY`
+ returns WebAuthn `passkeyOptions`. Complete with
+ `POST /sca/login/complete`.
- These accounts are used for platform-wide operations such as receiving funds from external sources or managing platform-level payment destinations.
- operationId: listPlatformExternalAccounts
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startScaLogin
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: currency
- in: query
- description: Filter by currency code
- required: false
- schema:
- type: string
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ScaLoginStartRequest'
responses:
'200':
- description: Successful operation
+ description: SCA login started; factor-specific material is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccountListResponse'
+ $ref: '#/components/schemas/ScaLoginStart'
'400':
- description: Bad request - Invalid parameters
+ description: Invalid or unknown factor
content:
application/json:
schema:
@@ -1517,18 +1424,47 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /sca/login/complete:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer completing an SCA login.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
post:
- summary: Add a new platform external account
- description: Register a new external bank account for the platform.
- operationId: createPlatformExternalAccount
+ summary: Complete an SCA login
+ description: |
+ Finalize an SCA login by submitting the proof for the started factor
+ (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for
+ `PASSKEY`), echoing the `challengeId` for `SMS_OTP`. Returns the
+ reported session status.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS/TOTP code is always `123456`.
+ operationId: completeScaLogin
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
requestBody:
@@ -1536,46 +1472,16 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/PlatformExternalAccountCreateRequest'
- examples:
- usBankAccount:
- summary: Create external US bank account
- value:
- currency: USD
- accountInfo:
- accountType: USD_ACCOUNT
- accountNumber: '12345678901'
- routingNumber: '123456789'
- bankAccountType: CHECKING
- bankName: Chase Bank
- platformAccountId: ext_acc_123456
- beneficiary:
- beneficiaryType: INDIVIDUAL
- fullName: John Doe
- birthDate: '1990-01-15'
- nationality: US
- address:
- line1: 123 Main Street
- city: San Francisco
- state: CA
- postalCode: '94105'
- country: US
- sparkWallet:
- summary: Create external Spark wallet
- value:
- currency: BTC
- accountInfo:
- accountType: SPARK_WALLET
- address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
+ $ref: '#/components/schemas/ScaLoginCompleteRequest'
responses:
- '201':
- description: External account created successfully
+ '200':
+ description: SCA login completed; the session status is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccount'
+ $ref: '#/components/schemas/ScaLoginComplete'
'400':
- description: Bad request
+ description: Invalid or expired proof
content:
application/json:
schema:
@@ -1586,41 +1492,70 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'409':
- description: Conflict - External account already exists
+ description: SCA is not required for this customer.
content:
application/json:
schema:
$ref: '#/components/schemas/Error409'
+ '423':
+ description: The customer's login is locked (or suspended) after too many failed attempts. `details.lockedUntil` says when they may retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error423'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /platform/external-accounts/{externalAccountId}:
+ /sca/record-event:
parameters:
- - name: externalAccountId
- in: path
- description: System-generated unique external account identifier
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer the security event is recorded for.
required: true
schema:
type: string
- get:
- summary: Get platform external account by ID
- description: Retrieve a platform external account by its system-generated ID
- operationId: getPlatformExternalAccountById
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ post:
+ summary: Record a security event
+ description: |
+ Record a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view), to
+ feed adaptive-authentication signals.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: recordSecurityEvent
tags:
- - External Accounts
+ - Strong Customer Authentication
security:
- BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RecordSecurityEventRequest'
responses:
'200':
- description: Successful operation
+ description: Event recorded; the customer's resulting login-security state is returned (including any lockout).
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccount'
+ $ref: '#/components/schemas/RecordSecurityEventResponse'
+ '400':
+ description: Invalid event type
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -1628,54 +1563,52 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: External account not found
+ description: Customer not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- delete:
- summary: Delete platform external account by ID
- description: Delete a platform external account by its system-generated ID
- operationId: deletePlatformExternalAccountById
- tags:
- - External Accounts
- security:
- - BasicAuth: []
- responses:
- '204':
- description: External account deleted successfully
- '401':
- description: Unauthorized
+ '409':
+ description: SCA is not required for this customer.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: External account not found
+ $ref: '#/components/schemas/Error409'
+ '423':
+ description: The customer's login is locked (or suspended) after too many failed attempts. `details.lockedUntil` says when they may retry.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error423'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /beneficial-owners:
+ /sca/factors/reset:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer resetting a factor.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
post:
- summary: Create a beneficial owner
+ summary: Start a 2FA reset
description: |
- Add a beneficial owner, director, or company officer to a business customer. The beneficial owner will go through KYC verification automatically.
- operationId: createBeneficialOwner
+ Begin recovering a lost enrolled factor via a liveness-gated, poll-based
+ flow. Opens the liveness check and returns a `resetId` plus the
+ opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end
+ user completes it with. Poll
+ `GET /sca/factors/reset/{resetId}` until liveness
+ passes, then call the complete endpoint.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startTwoFactorReset
tags:
- - KYC/KYB Verifications
+ - Strong Customer Authentication
security:
- BasicAuth: []
requestBody:
@@ -1683,16 +1616,16 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/BeneficialOwnerCreateRequest'
+ $ref: '#/components/schemas/TwoFactorResetStartRequest'
responses:
'201':
- description: Beneficial owner created successfully
+ description: Reset initiated; the reset handle and liveness material are returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/BeneficialOwner'
+ $ref: '#/components/schemas/TwoFactorResetStart'
'400':
- description: Bad request - Invalid parameters
+ description: Invalid or unknown factor
content:
application/json:
schema:
@@ -1709,139 +1642,122 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '429':
+ description: Too many reset attempts. Reset initiation is rate-limited to 5 per 24 hours per customer; retry after the window indicated by `Retry-After`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /sca/factors/reset/{resetId}:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer whose reset status is polled.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ - name: resetId
+ in: path
+ description: The reset handle returned by the start call.
+ required: true
+ schema:
+ type: string
get:
- summary: List beneficial owners
+ summary: Get 2FA reset status
description: |
- Retrieve a list of beneficial owners for a business customer.
- operationId: listBeneficialOwners
+ Poll the status of an in-progress 2FA reset until it reaches the liveness-passed value, after which the reset can be completed.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: getTwoFactorResetStatus
tags:
- - KYC/KYB Verifications
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: customerId
- in: query
- description: The business customer ID
- required: true
- schema:
- type: string
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
responses:
'200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/BeneficialOwnerListResponse'
- '400':
- description: Bad request - Invalid parameters
+ description: The current reset status.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/TwoFactorResetStatus'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /beneficial-owners/{beneficialOwnerId}:
- get:
- summary: Get a beneficial owner
- description: Retrieve details of a specific beneficial owner by ID.
- operationId: getBeneficialOwner
- tags:
- - KYC/KYB Verifications
- security:
- - BasicAuth: []
- parameters:
- - name: beneficialOwnerId
- in: path
- description: Beneficial owner ID
- required: true
- schema:
- type: string
- responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/BeneficialOwner'
- '401':
- description: Unauthorized
+ '404':
+ description: Customer or reset not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Beneficial owner not found
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- patch:
- summary: Update a beneficial owner
- description: Update details of a specific beneficial owner. Only provided fields are updated.
- operationId: updateBeneficialOwner
+ /sca/factors/reset/{resetId}/complete:
+ parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer completing the reset.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ - name: resetId
+ in: path
+ description: The reset handle returned by the start call.
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Complete a 2FA reset
+ description: |
+ Complete a 2FA reset once liveness has passed, clearing the lost factor so
+ the customer can re-enroll.
+
+ For an `SMS_OTP` reset, supply the new `mobile` number in the body — completing
+ the reset enrolls it as the customer's number. Other factors need no body.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: completeTwoFactorReset
tags:
- - KYC/KYB Verifications
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: beneficialOwnerId
- in: path
- description: Beneficial owner ID
- required: true
- schema:
- type: string
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/BeneficialOwnerUpdateRequest'
+ $ref: '#/components/schemas/TwoFactorResetCompleteRequest'
responses:
- '200':
- description: Beneficial owner updated successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/BeneficialOwner'
+ '204':
+ description: Reset completed; no content is returned.
'400':
- description: Bad request - Invalid parameters
+ description: Reset not ready (liveness not yet passed)
content:
application/json:
schema:
@@ -1853,40 +1769,54 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Beneficial owner not found
+ description: Customer or reset not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /documents:
+ /customers/external-accounts/{externalAccountId}/trust:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being trusted.
+ required: true
+ schema:
+ type: string
post:
- summary: Upload a document
+ summary: Start trusting a beneficiary
description: |
- Upload a verification document for a customer or beneficial owner. The request must use multipart/form-data with the file in the `file` field and metadata in the remaining fields.
+ Begin trusting (whitelisting) an external account so future sends to it can
+ skip the per-transaction SCA ceremony. Returns the `scaChallenge` to satisfy
+ (when one is issued). Complete with
+ `POST /customers/external-accounts/{externalAccountId}/trust/confirm`.
- Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB.
- operationId: uploadDocument
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startBeneficiaryTrust
tags:
- - Documents
+ - Strong Customer Authentication
security:
- BasicAuth: []
- requestBody:
- $ref: '#/components/requestBodies/DocumentUploadRequestBody'
responses:
- '201':
- description: Document uploaded successfully
+ '200':
+ description: Beneficiary trust started; the SCA challenge (if any) is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/Document'
+ $ref: '#/components/schemas/BeneficiaryTrustStart'
'400':
- description: Bad request - Invalid file type, size, or parameters
+ description: Invalid request
content:
application/json:
schema:
@@ -1898,57 +1828,62 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Document holder not found
+ description: Customer or external account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- get:
- summary: List documents
+ /customers/external-accounts/{externalAccountId}/trust/confirm:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being trusted.
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Confirm trusting a beneficiary
description: |
- Retrieve a list of documents with optional filtering by document holder.
- operationId: listDocuments
+ Finalize trusting a beneficiary (identified by the `externalAccountId` in the
+ path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or
+ `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when
+ one was issued. Returns `trusted: true`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS/TOTP code is always `123456`.
+ operationId: confirmBeneficiaryTrust
tags:
- - Documents
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: documentHolder
- in: query
- description: Filter by document holder ID (Customer or BeneficialOwner)
- required: false
- schema:
- type: string
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest'
responses:
'200':
- description: Successful operation
+ description: Beneficiary trusted.
content:
application/json:
schema:
- $ref: '#/components/schemas/DocumentListResponse'
+ $ref: '#/components/schemas/BeneficiaryTrustConfirm'
'400':
- description: Bad request - Invalid parameters
+ description: Invalid or expired proof
content:
application/json:
schema:
@@ -1959,35 +1894,60 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer or external account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /documents/{documentId}:
- get:
- summary: Get a document by ID
- description: Retrieve details and metadata of a specific document by ID.
- operationId: getDocument
+ /customers/external-accounts/{externalAccountId}/untrust:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being untrusted.
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Start untrusting a beneficiary
+ description: |
+ Begin untrusting (removing the trusted mark from) an external account, so
+ future sends to it are dynamically linked and require the per-transaction SCA
+ ceremony again. Returns the `scaChallenge` to satisfy (when one is issued).
+ Complete with
+ `POST /customers/external-accounts/{externalAccountId}/untrust/confirm`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startBeneficiaryUntrust
tags:
- - Documents
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: documentId
- in: path
- description: Document ID
- required: true
- schema:
- type: string
responses:
'200':
- description: Successful operation
+ description: Beneficiary untrust started; the SCA challenge (if any) is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/Document'
+ $ref: '#/components/schemas/BeneficiaryTrustStart'
+ '400':
+ description: Invalid request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -1995,44 +1955,62 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Document not found
+ description: Customer or external account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- put:
- summary: Replace a document
+ /customers/external-accounts/{externalAccountId}/untrust/confirm:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being untrusted.
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Confirm untrusting a beneficiary
description: |
- Replace an existing document with a new file and/or updated metadata. This is useful when a document was rejected and needs to be re-uploaded. The request must use multipart/form-data.
- operationId: replaceDocument
+ Finalize untrusting a beneficiary (identified by the `externalAccountId` in
+ the path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or
+ `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when
+ one was issued. Returns `trusted: false`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS/TOTP code is always `123456`.
+ operationId: confirmBeneficiaryUntrust
tags:
- - Documents
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: documentId
- in: path
- description: Document ID
- required: true
- schema:
- type: string
requestBody:
- $ref: '#/components/requestBodies/DocumentReplaceRequestBody'
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BeneficiaryTrustConfirmRequest'
responses:
'200':
- description: Document replaced successfully
+ description: Beneficiary untrusted.
content:
application/json:
schema:
- $ref: '#/components/schemas/Document'
+ $ref: '#/components/schemas/BeneficiaryTrustConfirm'
'400':
- description: Bad request - Invalid file type, size, or parameters
+ description: Invalid or expired proof
content:
application/json:
schema:
@@ -2044,158 +2022,128 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Document not found
+ description: Customer or external account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- delete:
- summary: Delete a document
+ /customers/internal-accounts:
+ get:
+ summary: List Customer internal accounts
description: |
- Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable.
- operationId: deleteDocument
+ Retrieve a list of internal accounts with optional filtering parameters. Returns all
+ internal accounts that match the specified filters. If no filters are provided, returns all internal accounts
+ (paginated).
+
+ Internal accounts are created automatically when a customer is created based on the platform configuration.
+ operationId: listCustomerInternalAccounts
tags:
- - Documents
+ - Internal Accounts
security:
- BasicAuth: []
parameters:
- - name: documentId
- in: path
- description: Document ID
- required: true
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
+ schema:
+ type: string
+ - name: customerId
+ in: query
+ description: Filter by internal accounts associated with a specific customer
+ required: false
+ schema:
+ type: string
+ - name: type
+ in: query
+ description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts.
+ required: false
+ schema:
+ $ref: '#/components/schemas/InternalAccountType'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
schema:
type: string
responses:
- '204':
- description: Document deleted successfully
- '401':
- description: Unauthorized
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Document not found
+ $ref: '#/components/schemas/InternalAccountListResponse'
+ '400':
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Document cannot be deleted (already submitted for verification)
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/Error401'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /verifications:
- post:
- summary: Submit customer for verification
+ /platform/internal-accounts:
+ get:
+ summary: List platform internal accounts
description: |
- Trigger KYC (individual) or KYB (business) verification for a customer.
- The response indicates whether all required information has been provided.
- If data is missing, the `errors` array describes exactly what needs to be
- supplied before verification can proceed.
-
- Call this endpoint again after resolving errors to re-submit.
-
- ### What to collect for KYB
-
- Before submitting a `BUSINESS` customer, collect the following via
- `POST /customers`, `POST /beneficial-owners`, and `POST /documents`:
-
- **Business identifying information**
- - Entity full legal name
- - Doing Business As (DBA) name, if applicable
- - Physical address — principal place of business
- - Countries of operation
- - Identification number — U.S. taxpayer identification number, or, for a
- foreign business without one, alternative government-issued documentation
- certifying the existence of the business
-
- **Ownership and control structure** — collected for **one control person**
- (an individual with significant responsibility to control, manage, or
- direct the legal entity) **and all beneficial owners** (every individual
- who owns 25% or more, directly or indirectly). For each, provide:
- - Full name
- - Date of birth
- - Address
- - Identification number:
- - U.S. persons — SSN or ITIN
- - Non-U.S. persons — one or more of: ITIN, passport (with country of
- issuance), alien identification card, or another government-issued
- photo ID evidencing nationality or residence
+ Retrieve a list of all internal accounts that belong to the platform, as opposed to an individual customer.
- **Required documents**
- - Company formation and existence documents (certificate of incorporation,
- articles of association, etc.)
- - Proof of ownership and control structure (organization and ownership
- chart, shareholder agreements, operating agreements, register of members,
- or certification of controlling person and beneficial owners)
- - Proof of address dated within the last 3 months (utility bill, bank
- statement, lease agreement, or official correspondence)
- - Tax ID or equivalent identifying-number documents
- - For non-U.S. beneficial owners — passport plus one additional
- government-issued ID
- operationId: createVerification
+ These accounts are created automatically when the platform is configured for each supported currency. They can be used for things like distributing bitcoin rewards to customers, or for other platform-wide purposes.
+ operationId: listPlatformInternalAccounts
tags:
- - KYC/KYB Verifications
+ - Internal Accounts
security:
- BasicAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VerificationRequest'
+ parameters:
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
+ schema:
+ type: string
+ - name: type
+ in: query
+ description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for a customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for the platform-managed holding accounts.
+ required: false
+ schema:
+ $ref: '#/components/schemas/InternalAccountType'
responses:
'200':
- description: |
- Verification status returned. Check `verificationStatus` and `errors` to determine next steps.
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Verification'
- examples:
- missingInfo:
- summary: Verification blocked by missing data
- value:
- id: Verification:019542f5-b3e7-1d02-0000-000000000001
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- verificationStatus: RESOLVE_ERRORS
- errors:
- - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
- type: MISSING_FIELD
- field: customer.address.line1
- reason: Business address line 1 is required
- - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
- type: MISSING_PROOF_OF_ADDRESS_DOCUMENT
- acceptedDocumentTypes:
- - PROOF_OF_ADDRESS
- reason: Proof of address document is required
- - resourceId: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000002
- type: MISSING_FIELD
- field: personalInfo.birthDate
- reason: Date of birth is required for beneficial owners
- createdAt: '2025-10-03T12:00:00Z'
- submitted:
- summary: Verification submitted successfully
- value:
- id: Verification:019542f5-b3e7-1d02-0000-000000000002
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- verificationStatus: IN_PROGRESS
- errors: []
- createdAt: '2025-10-03T12:00:00Z'
+ $ref: '#/components/schemas/PlatformInternalAccountListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -2208,40 +2156,39 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Customer not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /customers/external-accounts:
get:
- summary: List verifications
+ summary: List Customer external accounts
description: |
- Retrieve a list of verifications with optional filtering by customer ID and status.
- operationId: listVerifications
+ Retrieve a list of external accounts with optional filtering parameters. Returns all
+ external accounts that match the specified filters. If no filters are provided, returns all external accounts
+ (paginated).
+
+ External accounts are bank accounts, cryptocurrency wallets, or other payment destinations that customers can use to receive funds from the platform.
+ operationId: listCustomerExternalAccounts
tags:
- - KYC/KYB Verifications
+ - External Accounts
security:
- BasicAuth: []
parameters:
- - name: customerId
+ - name: currency
in: query
- description: Filter by customer ID
+ description: Filter by currency code
required: false
schema:
type: string
- - name: verificationStatus
+ - name: customerId
in: query
- description: Filter by verification status
+ description: Filter by external accounts associated with a specific customer
required: false
schema:
- $ref: '#/components/schemas/VerificationStatus'
+ type: string
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -2263,7 +2210,7 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/VerificationListResponse'
+ $ref: '#/components/schemas/ExternalAccountListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -2282,90 +2229,61 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /verifications/{verificationId}:
- get:
- summary: Get a verification
- description: Retrieve details of a specific verification by ID.
- operationId: getVerification
+ post:
+ summary: Add a new external account
+ description: Register a new external bank account for a customer.
+ operationId: createCustomerExternalAccount
tags:
- - KYC/KYB Verifications
+ - External Accounts
security:
- BasicAuth: []
- parameters:
- - name: verificationId
- in: path
- description: Verification ID
- required: true
- schema:
- type: string
- responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Verification'
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Verification not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /transfer-in:
- post:
- summary: Create a transfer-in request
- description: |
- Transfer funds from an external account to an internal account for a specific customer. This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the paymentInstructions on the internal account to deposit funds.
- operationId: createTransferIn
- tags:
- - Same-Currency Transfers
- security:
- - BasicAuth: []
- parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
- schema:
- type: string
- example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/TransferInRequest'
+ $ref: '#/components/schemas/ExternalAccountCreateRequest'
examples:
- transferIn:
- summary: Transfer from external to internal account
+ usBankAccount:
+ summary: Create external US bank account
value:
- source:
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- amount: 12550
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ currency: USD
+ accountInfo:
+ accountType: USD_ACCOUNT
+ accountNumber: '12345678901'
+ routingNumber: '123456789'
+ bankAccountType: CHECKING
+ bankName: Chase Bank
+ platformAccountId: ext_acc_123456
+ beneficiary:
+ beneficiaryType: INDIVIDUAL
+ fullName: John Doe
+ birthDate: '1990-01-15'
+ nationality: US
+ address:
+ line1: 123 Main Street
+ city: San Francisco
+ state: CA
+ postalCode: '94105'
+ country: US
+ sparkWallet:
+ summary: Create external Spark wallet
+ value:
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ currency: BTC
+ accountInfo:
+ accountType: SPARK_WALLET
+ address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
responses:
'201':
- description: Transfer-in request created successfully
+ description: External account created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionOneOf'
+ $ref: '#/components/schemas/ExternalAccount'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request
content:
application/json:
schema:
@@ -2376,72 +2294,41 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Customer or account not found
+ '409':
+ description: Conflict - External account already exists
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transfer-out:
- post:
- summary: Create a transfer-out request
- description: |
- Transfer funds from an internal account to an external account for a specific customer.
- operationId: createTransferOut
+ /customers/external-accounts/{externalAccountId}:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: System-generated unique external account identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get customer external account by ID
+ description: Retrieve a customer external account by its system-generated ID
+ operationId: getCustomerExternalAccountById
tags:
- - Same-Currency Transfers
+ - External Accounts
security:
- BasicAuth: []
- parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
- schema:
- type: string
- example: 550e8400-e29b-41d4-a716-446655440000
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TransferOutRequest'
- examples:
- transferOut:
- summary: Transfer from internal to external account
- value:
- source:
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- destination:
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- paymentRail: ACH
- amount: 12550
- remittanceInformation: '12345'
responses:
- '201':
- description: |
- Transfer-out request created successfully.
-
- For customers outside SCA-regulated regions the transaction proceeds as usual.
-
- **Strong Customer Authentication (EU):** per-transaction SCA is authorized only on the quote resource (`POST /quotes/{quoteId}/authorize`), and `transfer-out` has no associated quote. `transfer-out` is being deprecated, so SCA-gated EU debits are not offered on this endpoint — use the quote + `execute` flow instead.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TransactionOneOf'
- '400':
- description: Bad request - Invalid parameters
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/ExternalAccount'
'401':
description: Unauthorized
content:
@@ -2449,7 +2336,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Customer or account not found
+ description: External account not found
content:
application/json:
schema:
@@ -2460,49 +2347,17 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /receiver/uma/{receiverUmaAddress}:
- get:
- summary: Look up an UMA address for payment
- description: |
- Lookup a receiving UMA address to determine supported currencies and exchange rates.
- This endpoint helps platforms determine what currencies they can send to a given UMA address.
- operationId: lookupUma
+ delete:
+ summary: Delete customer external account by ID
+ description: Delete a customer external account by its system-generated ID
+ operationId: deleteCustomerExternalAccountById
tags:
- - Cross-Currency Transfers
+ - External Accounts
security:
- BasicAuth: []
- parameters:
- - name: receiverUmaAddress
- in: path
- description: UMA address of the intended recipient
- required: true
- schema:
- type: string
- - name: senderUmaAddress
- in: query
- description: UMA address of the sender (optional if customerId is provided)
- required: false
- schema:
- type: string
- - name: customerId
- in: query
- description: System ID of the sender (optional if senderUmaAddress is provided)
- required: false
- schema:
- type: string
responses:
- '200':
- description: Successful lookup
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ReceiverUmaLookupResponse'
- '400':
- description: Bad request - Missing or invalid parameters
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
+ '204':
+ description: External account deleted successfully
'401':
description: Unauthorized
content:
@@ -2510,69 +2365,60 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: UMA address not found. The address is well-formed but no receiver exists at the counterparty VASP (`UMA_NOT_FOUND`).
+ description: External account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '412':
- description: Counterparty doesn't support UMA version
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error412'
- '424':
- description: Counterparty issue
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /receiver/external-account/{accountId}:
+ /platform/external-accounts:
get:
- summary: Look up an external account for payment
+ summary: List platform external accounts
description: |
- Lookup an external account by ID to determine supported currencies and exchange rates.
- This endpoint helps platforms determine what currencies they can send to a given external account, along with the current estimated exchange rates and minimum and maximum amounts that can be sent.
- operationId: lookupExternalAccount
+ Retrieve a list of all external accounts that belong to the platform, as opposed to an individual customer.
+
+ These accounts are used for platform-wide operations such as receiving funds from external sources or managing platform-level payment destinations.
+ operationId: listPlatformExternalAccounts
tags:
- - Cross-Currency Transfers
+ - External Accounts
security:
- BasicAuth: []
parameters:
- - name: accountId
- in: path
- description: System-generated ID of the external account
- required: true
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
schema:
type: string
- example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- - name: senderUmaAddress
+ - name: limit
in: query
- description: UMA address of the sender (optional if customerId is provided)
+ description: Maximum number of results to return (default 20, max 100)
required: false
schema:
- type: string
- - name: customerId
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
in: query
- description: System ID of the sender (optional if senderUmaAddress is provided)
+ description: Cursor for pagination (returned from previous request)
required: false
schema:
type: string
responses:
'200':
- description: Successful lookup
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse'
+ $ref: '#/components/schemas/ExternalAccountListResponse'
'400':
- description: Bad request - Missing or invalid parameters
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -2583,56 +2429,110 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: External account not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '412':
- description: Counterparty doesn't support UMA version
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error412'
- '424':
- description: Counterparty issue
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /quotes/{quoteId}:
- get:
- summary: Get quote by ID
- description: |
- Retrieve a quote by its ID. If the quote has been settled, it will include
- the transaction ID. This allows clients to track the full lifecycle of a payment
- from quote creation to settlement.
- operationId: getQuoteById
- tags:
- - Cross-Currency Transfers
- security:
+ post:
+ summary: Add a new platform external account
+ description: Register a new external bank account for the platform.
+ operationId: createPlatformExternalAccount
+ tags:
+ - External Accounts
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PlatformExternalAccountCreateRequest'
+ examples:
+ usBankAccount:
+ summary: Create external US bank account
+ value:
+ currency: USD
+ accountInfo:
+ accountType: USD_ACCOUNT
+ accountNumber: '12345678901'
+ routingNumber: '123456789'
+ bankAccountType: CHECKING
+ bankName: Chase Bank
+ platformAccountId: ext_acc_123456
+ beneficiary:
+ beneficiaryType: INDIVIDUAL
+ fullName: John Doe
+ birthDate: '1990-01-15'
+ nationality: US
+ address:
+ line1: 123 Main Street
+ city: San Francisco
+ state: CA
+ postalCode: '94105'
+ country: US
+ sparkWallet:
+ summary: Create external Spark wallet
+ value:
+ currency: BTC
+ accountInfo:
+ accountType: SPARK_WALLET
+ address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
+ responses:
+ '201':
+ description: External account created successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExternalAccount'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - External account already exists
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /platform/external-accounts/{externalAccountId}:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: System-generated unique external account identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get platform external account by ID
+ description: Retrieve a platform external account by its system-generated ID
+ operationId: getPlatformExternalAccountById
+ tags:
+ - External Accounts
+ security:
- BasicAuth: []
- parameters:
- - name: quoteId
- in: path
- description: ID of the quote to retrieve
- required: true
- schema:
- type: string
responses:
'200':
- description: Quote retrieved successfully
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/ExternalAccount'
'401':
description: Unauthorized
content:
@@ -2640,7 +2540,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Quote not found
+ description: External account not found
content:
application/json:
schema:
@@ -2651,132 +2551,60 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /quotes:
+ delete:
+ summary: Delete platform external account by ID
+ description: Delete a platform external account by its system-generated ID
+ operationId: deletePlatformExternalAccountById
+ tags:
+ - External Accounts
+ security:
+ - BasicAuth: []
+ responses:
+ '204':
+ description: External account deleted successfully
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: External account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /beneficial-owners:
post:
- summary: Create a transfer quote
+ summary: Create a beneficial owner
description: |
- Generate a quote for a cross-currency transfer between any combination of accounts
- and UMA addresses. This endpoint handles currency exchange and provides the necessary
- instructions to execute the transfer.
-
- **Transfer Types Supported:**
- - **Account to Account**: Transfer between internal/external accounts with currency exchange.
- - **Account to UMA**: Transfer from an internal account to an UMA address.
- - **UMA to Account or UMA to UMA**: This transfer type will only be funded by payment instructions, not from an internal account.
-
- **Key Features:**
- - **Flexible Amount Locking**: Always specify whether you want to lock the sending amount or receiving amount
- - **Currency Exchange**: Handles all cross-currency transfers with real-time exchange rates
- - **Payment Instructions**: For UMA or customer ID sources, provides banking details needed for execution
-
- **Important:** If you are transferring funds in the same currency (no exchange required),
- use the `/transfer-in` or `/transfer-out` endpoints instead.
- operationId: createQuote
+ Add a beneficial owner, director, or company officer to a business customer. The beneficial owner will go through KYC verification automatically.
+ operationId: createBeneficialOwner
tags:
- - Cross-Currency Transfers
+ - KYC/KYB Verifications
security:
- BasicAuth: []
- parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
- schema:
- type: string
- example:
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/QuoteRequest'
- examples:
- accountToAccount:
- summary: Account to Account Transfer
- value:
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- lockedCurrencySide: SENDING
- lockedCurrencyAmount: 10000
- description: Transfer between accounts, either internal or external.
- accountToUma:
- summary: Account to UMA Address Transfer
- value:
- lookupId: LookupRequest:019542f5-b3e7-1d02-0000-000000000009
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: UMA_ADDRESS
- umaAddress: $receiver@uma.domain.com
- currency: EUR
- lockedCurrencySide: SENDING
- lockedCurrencyAmount: 1000
- description: 'Payment for invoice #1234'
- realTimeFundingToSparkWallet:
- summary: Real-time funding to Spark Wallet as an on-ramp flow. Immediate execution.
- value:
- source:
- sourceType: REALTIME_FUNDING
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000009
- currency: USD
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456
- lockedCurrencySide: RECEIVING
- lockedCurrencyAmount: 10000
- immediatelyExecute: true
- description: Bitcoin reward payout!
+ $ref: '#/components/schemas/BeneficialOwnerCreateRequest'
responses:
'201':
- description: |
- Transfer quote created successfully. The response includes exchange rates,
- fees, and transfer details. For transfers involving UMA addresses, payment
- instructions are also included for execution through banking systems.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Quote'
- example:
- id: Quote:019542f5-b3e7-1d02-0000-000000000006
- status: PENDING
- createdAt: '2025-10-03T12:00:00Z'
- expiresAt: '2025-10-03T12:05:00Z'
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- sendingCurrency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- receivingCurrency:
- code: EUR
- name: Euro
- symbol: €
- decimals: 2
- totalSendingAmount: 10000
- totalReceivingAmount: 9200
- exchangeRate: 0.92
- feesIncluded: 10
- transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005
- '202':
- description: |
- Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated.
+ description: Beneficial owner created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/BeneficialOwner'
'400':
- description: Bad request - Missing or invalid parameters
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -2787,172 +2615,145 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '412':
- description: Counterparty doesn't support UMA version
+ '404':
+ description: Customer not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error412'
- '424':
- description: Counterparty issue
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error424'
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error501'
- /quotes/{quoteId}/execute:
- post:
- summary: Execute a quote
+ get:
+ summary: List beneficial owners
description: |
- Execute a quote by its ID. This endpoint initiates the transfer between
- the source and destination accounts.
-
- This endpoint can only be used for quotes with a `source` which is either an internal account,
- or has direct pull functionality (e.g. ACH pull with an external account).
-
- When the quote's `source` is an internal account of type `EMBEDDED_WALLET`,
- the request must include a `Grid-Wallet-Signature` header. The header value
- is the full Grid wallet signature built over the `payloadToSign` value from
- the quote's `paymentInstructions[].accountOrWalletInfo` entry with the
- session private key of a verified authentication credential on the source
- Embedded Wallet.
-
- Once executed, the quote cannot be cancelled and the transfer will be processed.
- operationId: executeQuote
+ Retrieve a list of beneficial owners for a business customer.
+ operationId: listBeneficialOwners
tags:
- - Cross-Currency Transfers
+ - KYC/KYB Verifications
security:
- BasicAuth: []
parameters:
- - name: quoteId
- in: path
+ - name: customerId
+ in: query
+ description: The business customer ID
required: true
- description: The unique identifier of the quote to execute
schema:
type: string
- example: Quote:019542f5-b3e7-1d02-0000-000000000001
- - name: Idempotency-Key
- in: header
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
schema:
- type: string
- example:
- - name: Grid-Wallet-Signature
- in: header
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
required: false
- description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
schema:
type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- requestBody:
- required: false
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ExecuteQuoteRequest'
responses:
'200':
- description: |
- Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers):
-
- - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`).
-
- - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`.
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/BeneficialOwnerListResponse'
'400':
- description: Bad request - Invalid quote ID or quote cannot be confirmed
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Quote not found
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Quote already confirmed, expired, or in invalid state
+ $ref: '#/components/schemas/Error500'
+ /beneficial-owners/{beneficialOwnerId}:
+ get:
+ summary: Get a beneficial owner
+ description: Retrieve details of a specific beneficial owner by ID.
+ operationId: getBeneficialOwner
+ tags:
+ - KYC/KYB Verifications
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: beneficialOwnerId
+ in: path
+ description: Beneficial owner ID
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/BeneficialOwner'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Beneficial owner not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /quotes/{quoteId}/authorize:
- parameters:
- - name: quoteId
- in: path
- description: The unique identifier of the quote whose SCA challenge is being authorized.
- required: true
- schema:
- type: string
- example: Quote:019542f5-b3e7-1d02-0000-000000000006
- post:
- summary: Authorize a quote's SCA challenge
- description: |
- Satisfy the Strong Customer Authentication challenge carried by a quote in
- `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof.
-
- This is used for realtime-funding quotes: the quote is returned with an
- `scaChallenge` and **without** `paymentInstructions`; once authorized, the
- quote advances and its `paymentInstructions` are populated so the customer
- can fund the transfer.
-
- As with all SCA, a quote may require more than one authorization: after
- authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the
- next `scaChallenge` — authorize that too, repeating until it advances (see
- `ScaChallenge`).
-
- This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
-
- In sandbox, the SMS code is always `123456`.
- operationId: authorizeQuote
+ patch:
+ summary: Update a beneficial owner
+ description: Update details of a specific beneficial owner. Only provided fields are updated.
+ operationId: updateBeneficialOwner
tags:
- - Strong Customer Authentication
+ - KYC/KYB Verifications
security:
- BasicAuth: []
+ parameters:
+ - name: beneficialOwnerId
+ in: path
+ description: Beneficial owner ID
+ required: true
+ schema:
+ type: string
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/ScaAuthorization'
+ $ref: '#/components/schemas/BeneficialOwnerUpdateRequest'
responses:
'200':
- description: Challenge authorized; the updated quote is returned.
+ description: Beneficial owner updated successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/BeneficialOwner'
'400':
- description: Invalid or expired authorization proof
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -2964,169 +2765,78 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Quote not found
+ description: Beneficial owner not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '409':
- description: SCA is not required for this customer, or the quote has no pending challenge to authorize.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error409'
- '429':
- description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /quotes/{quoteId}/authorize/resend:
- parameters:
- - name: quoteId
- in: path
- description: The unique identifier of the quote whose SCA challenge code should be re-sent.
- required: true
- schema:
- type: string
- example: Quote:019542f5-b3e7-1d02-0000-000000000006
+ /documents:
post:
- summary: Resend a quote's SCA challenge code
+ summary: Upload a document
description: |
- Re-send the one-time code for a realtime-funding quote in
- `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The
- existing challenge is reused — no new challenge is issued, and its
- `scaChallenge.expiresAt` is **not** extended; once the challenge is past
- `expiresAt` it can no longer be authorized.
-
- Only meaningful for customers in a region where SCA is required (e.g. EU);
- a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409.
+ Upload a verification document for a customer or beneficial owner. The request must use multipart/form-data with the file in the `file` field and metadata in the remaining fields.
- In sandbox, the code is always `123456`.
- operationId: resendQuoteScaCode
+ Supported file types: PDF, JPEG, PNG. Maximum file size: 10 MB.
+ operationId: uploadDocument
tags:
- - Strong Customer Authentication
+ - Documents
security:
- BasicAuth: []
+ requestBody:
+ $ref: '#/components/requestBodies/DocumentUploadRequestBody'
responses:
- '204':
- description: Code re-sent.
- '401':
- description: Unauthorized
+ '201':
+ description: Document uploaded successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Quote not found
+ $ref: '#/components/schemas/Document'
+ '400':
+ description: Bad request - Invalid file type, size, or parameters
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey).
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- '429':
- description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Document holder not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error429'
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions:
get:
- summary: List transactions
+ summary: List documents
description: |
- Retrieve a paginated list of transactions with optional filtering.
- The transactions can be filtered by customer ID, platform customer ID, UMA address,
- date range, status, and transaction type.
-
- Card transactions are included and identified by `type: CARD`. In Sandbox this is how
- you discover a `CardTransaction` id after simulating an authorization — list the
- transactions, take the card transaction's `id`, and pass it as the `cardTransactionId`
- to the clearing and return simulate endpoints.
- operationId: listTransactions
- tags:
- - Transactions
+ Retrieve a list of documents with optional filtering by document holder.
+ operationId: listDocuments
+ tags:
+ - Documents
security:
- BasicAuth: []
parameters:
- - name: customerId
- in: query
- description: Filter by system customer ID. To filter to transactions made on behalf of the platform, specify the platform ID as the customer ID.
- required: false
- schema:
- type: string
- - name: platformCustomerId
- in: query
- description: Filter by platform-specific customer ID
- required: false
- schema:
- type: string
- - name: accountIdentifier
- in: query
- description: Filter by account identifier (matches either sender or receiver)
- required: false
- schema:
- type: string
- - name: senderAccountIdentifier
- in: query
- description: Filter by sender account identifier
- required: false
- schema:
- type: string
- - name: receiverAccountIdentifier
- in: query
- description: Filter by receiver account identifier
- required: false
- schema:
- type: string
- - name: status
- in: query
- description: Filter by transaction status
- required: false
- schema:
- $ref: '#/components/schemas/TransactionStatus'
- - name: type
- in: query
- description: Filter by transaction type
- required: false
- schema:
- $ref: '#/components/schemas/TransactionType'
- - name: reference
- in: query
- description: Filter by reference
- required: false
- schema:
- type: string
- - name: startDate
- in: query
- description: Filter by start date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: endDate
+ - name: documentHolder
in: query
- description: Filter by end date (inclusive) in ISO 8601 format
+ description: Filter by document holder ID (Customer or BeneficialOwner)
required: false
schema:
type: string
- format: date-time
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -3142,23 +2852,13 @@ paths:
required: false
schema:
type: string
- - name: sortOrder
- in: query
- description: Order to sort results in
- required: false
- schema:
- type: string
- enum:
- - asc
- - desc
- default: desc
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionListResponse'
+ $ref: '#/components/schemas/DocumentListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -3177,29 +2877,29 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions/{transactionId}:
- parameters:
- - name: transactionId
- in: path
- description: Unique identifier of the transaction
- required: true
- schema:
- type: string
+ /documents/{documentId}:
get:
- summary: Get transaction by ID
- description: Retrieve detailed information about a specific transaction.
- operationId: getTransactionById
+ summary: Get a document by ID
+ description: Retrieve details and metadata of a specific document by ID.
+ operationId: getDocument
tags:
- - Transactions
+ - Documents
security:
- BasicAuth: []
+ parameters:
+ - name: documentId
+ in: path
+ description: Document ID
+ required: true
+ schema:
+ type: string
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionOneOf'
+ $ref: '#/components/schemas/Document'
'401':
description: Unauthorized
content:
@@ -3207,7 +2907,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Transaction not found
+ description: Document not found
content:
application/json:
schema:
@@ -3218,38 +2918,33 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions/{transactionId}/confirm:
- post:
- summary: Confirm receipt delivery
+ put:
+ summary: Replace a document
description: |
- Confirm that the platform delivered the transaction receipt to its customer. This confirmation is only necessary when the platform is contractually required to send a receipt. If `receiptDeliveryConfirmedAt` is omitted, the confirmation time is set to the current server time. Calling this endpoint again for the same transaction updates the stored confirmation time.
- operationId: confirmReceiptDelivery
+ Replace an existing document with a new file and/or updated metadata. This is useful when a document was rejected and needs to be re-uploaded. The request must use multipart/form-data.
+ operationId: replaceDocument
tags:
- - Transactions
+ - Documents
security:
- BasicAuth: []
parameters:
- - name: transactionId
+ - name: documentId
in: path
- description: Unique identifier of the transaction to confirm receipt delivery for
+ description: Document ID
required: true
schema:
type: string
requestBody:
- required: false
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ConfirmReceiptDeliveryRequest'
+ $ref: '#/components/requestBodies/DocumentReplaceRequestBody'
responses:
'200':
- description: Receipt delivery confirmation recorded successfully
+ description: Document replaced successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionOneOf'
+ $ref: '#/components/schemas/Document'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request - Invalid file type, size, or parameters
content:
application/json:
schema:
@@ -3261,7 +2956,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Transaction not found
+ description: Document not found
content:
application/json:
schema:
@@ -3272,43 +2967,25 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions/{transactionId}/approve:
- post:
- summary: Approve a pending incoming payment
+ delete:
+ summary: Delete a document
description: |
- Approve a pending incoming payment that was previously acknowledged with a 202 response.
- This endpoint allows platforms to asynchronously approve payments after async processing.
- operationId: approvePendingPayment
+ Delete an uploaded document. This cannot be undone. Documents that have already been submitted for verification may not be deletable.
+ operationId: deleteDocument
tags:
- - Transactions
+ - Documents
security:
- BasicAuth: []
parameters:
- - name: transactionId
+ - name: documentId
in: path
- description: Unique identifier of the transaction to approve
+ description: Document ID
required: true
schema:
type: string
- requestBody:
- required: false
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ApprovePaymentRequest'
responses:
- '200':
- description: Payment approved successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/IncomingTransaction'
- '400':
- description: Bad request - Invalid parameters or payment cannot be approved
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
+ '204':
+ description: Document deleted successfully
'401':
description: Unauthorized
content:
@@ -3316,13 +2993,13 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Transaction not found
+ description: Document not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
'409':
- description: Conflict - Payment is not in a pending state or has already been processed or timed out.
+ description: Conflict - Document cannot be deleted (already submitted for verification)
content:
application/json:
schema:
@@ -3333,99 +3010,172 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /transactions/{transactionId}/reject:
+ /verifications:
post:
- summary: Reject a pending incoming payment
+ summary: Submit customer for verification
description: |
- Reject a pending incoming payment that was previously acknowledged with a 202 response.
- This endpoint allows platforms to asynchronously reject payments after additional processing.
- operationId: rejectPendingPayment
+ Trigger KYC (individual) or KYB (business) verification for a customer.
+ The response indicates whether all required information has been provided.
+ If data is missing, the `errors` array describes exactly what needs to be
+ supplied before verification can proceed.
+
+ Call this endpoint again after resolving errors to re-submit.
+
+ ### What to collect for KYB
+
+ Before submitting a `BUSINESS` customer, collect the following via
+ `POST /customers`, `POST /beneficial-owners`, and `POST /documents`:
+
+ **Business identifying information**
+ - Entity full legal name
+ - Doing Business As (DBA) name, if applicable
+ - Physical address — principal place of business
+ - Countries of operation
+ - Identification number — U.S. taxpayer identification number, or, for a
+ foreign business without one, alternative government-issued documentation
+ certifying the existence of the business
+
+ **Ownership and control structure** — collected for **one control person**
+ (an individual with significant responsibility to control, manage, or
+ direct the legal entity) **and all beneficial owners** (every individual
+ who owns 25% or more, directly or indirectly). For each, provide:
+ - Full name
+ - Date of birth
+ - Address
+ - Identification number:
+ - U.S. persons — SSN or ITIN
+ - Non-U.S. persons — one or more of: ITIN, passport (with country of
+ issuance), alien identification card, or another government-issued
+ photo ID evidencing nationality or residence
+
+ **Required documents**
+ - Company formation and existence documents (certificate of incorporation,
+ articles of association, etc.)
+ - Proof of ownership and control structure (organization and ownership
+ chart, shareholder agreements, operating agreements, register of members,
+ or certification of controlling person and beneficial owners)
+ - Proof of address dated within the last 3 months (utility bill, bank
+ statement, lease agreement, or official correspondence)
+ - Tax ID or equivalent identifying-number documents
+ - For non-U.S. beneficial owners — passport plus one additional
+ government-issued ID
+ operationId: createVerification
tags:
- - Transactions
+ - KYC/KYB Verifications
security:
- BasicAuth: []
- parameters:
- - name: transactionId
- in: path
- description: Unique identifier of the transaction to reject
- required: true
- schema:
- type: string
requestBody:
- required: false
+ required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/RejectPaymentRequest'
+ $ref: '#/components/schemas/VerificationRequest'
responses:
'200':
- description: Payment rejected successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/IncomingTransaction'
- '400':
- description: Bad request - Invalid parameters or payment cannot be rejected
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
+ description: |
+ Verification status returned. Check `verificationStatus` and `errors` to determine next steps.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Transaction not found
+ $ref: '#/components/schemas/Verification'
+ examples:
+ missingInfo:
+ summary: Verification blocked by missing data
+ value:
+ id: Verification:019542f5-b3e7-1d02-0000-000000000001
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ verificationStatus: RESOLVE_ERRORS
+ errors:
+ - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ type: MISSING_FIELD
+ field: customer.address.line1
+ reason: Business address line 1 is required
+ - resourceId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ type: MISSING_PROOF_OF_ADDRESS_DOCUMENT
+ acceptedDocumentTypes:
+ - PROOF_OF_ADDRESS
+ reason: Proof of address document is required
+ - resourceId: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000002
+ type: MISSING_FIELD
+ field: personalInfo.birthDate
+ reason: Date of birth is required for beneficial owners
+ createdAt: '2025-10-03T12:00:00Z'
+ submitted:
+ summary: Verification submitted successfully
+ value:
+ id: Verification:019542f5-b3e7-1d02-0000-000000000002
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ verificationStatus: IN_PROGRESS
+ errors: []
+ createdAt: '2025-10-03T12:00:00Z'
+ '400':
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Payment is not in a pending state or has already been processed or timed out.
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /crypto/estimate-withdrawal-fee:
- post:
- summary: Estimate crypto withdrawal fee
+ get:
+ summary: List verifications
description: |
- Estimate the network and application fees for a cryptocurrency withdrawal from a crypto internal account to an external blockchain address. Use this to show fee information to customers before they initiate a withdrawal.
- operationId: estimateCryptoWithdrawalFee
+ Retrieve a list of verifications with optional filtering by customer ID and status.
+ operationId: listVerifications
tags:
- - Cross-Currency Transfers
+ - KYC/KYB Verifications
security:
- BasicAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeRequest'
- examples:
- estimateUSDCWithdrawal:
- summary: Estimate fee for USDC withdrawal on Solana
- value:
- internalAccountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- currency: USDC
- cryptoNetwork: SOLANA
- amount: 1000000
- destinationAddress: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU
+ parameters:
+ - name: customerId
+ in: query
+ description: Filter by customer ID
+ required: false
+ schema:
+ type: string
+ - name: verificationStatus
+ in: query
+ description: Filter by verification status
+ required: false
+ schema:
+ $ref: '#/components/schemas/VerificationStatus'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Fee estimation returned successfully
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeResponse'
+ $ref: '#/components/schemas/VerificationListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -3438,216 +3188,164 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Internal account not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/webhooks/test:
- post:
- summary: Send a test webhook
- description: Send a test webhook to the configured endpoint
- operationId: sendTestWebhook
+ /verifications/{verificationId}:
+ get:
+ summary: Get a verification
+ description: Retrieve details of a specific verification by ID.
+ operationId: getVerification
tags:
- - Sandbox
+ - KYC/KYB Verifications
security:
- BasicAuth: []
+ parameters:
+ - name: verificationId
+ in: path
+ description: Verification ID
+ required: true
+ schema:
+ type: string
responses:
'200':
- description: Webhook delivered successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TestWebhookResponse'
- '400':
- description: Bad request - Webhook delivery failed
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/Verification'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Verification not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/bulk/csv:
+ /transfer-in:
post:
- summary: Upload customers via CSV file
+ summary: Create a transfer-in request
description: |
- Upload a CSV file containing customer information for bulk creation. The CSV file should follow
- a specific format with required and optional columns based on customer type.
-
- ### CSV Format
- The CSV file should have the following columns:
-
- Required columns for all customers:
- - umaAddress: The customer's UMA address (e.g., $john.doe@uma.domain.com)
- - platformCustomerId: Your platform's unique identifier for the customer
- - customerType: Either "INDIVIDUAL" or "BUSINESS"
-
- Required columns for individual customers:
- - fullName: Individual's full name
- - birthDate: Date of birth in YYYY-MM-DD format
- - addressLine1: Street address line 1
- - city: City
- - state: State/Province/Region
- - postalCode: Postal/ZIP code
- - country: Country code (ISO 3166-1 alpha-2)
-
- Required columns for business customers:
- - businessLegalName: Legal name of the business
- - addressLine1: Street address line 1
- - city: City
- - state: State/Province/Region
- - postalCode: Postal/ZIP code
- - country: Country code (ISO 3166-1 alpha-2)
-
- Optional columns for all customers:
- - addressLine2: Street address line 2
- - platformAccountId: Your platform's identifier for the bank account
- - description: Optional description for the customer
-
- Optional columns for individual customers:
- - email: Customer's email address
-
- Optional columns for business customers:
- - businessRegistrationNumber: Business registration number
- - businessTaxId: Tax identification number
-
- ### Example CSV
- ```csv
- umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,platformAccountId,businessLegalName
- john.doe@uma.domain.com,customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US
- acme@uma.domain.com,biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US
- ```
-
- The upload process is asynchronous and will return a job ID that can be used to track progress.
- You can monitor the job status using the `/customers/bulk/jobs/{jobId}` endpoint.
- operationId: uploadCustomersCsv
+ Transfer funds from an external account to an internal account for a specific customer. This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the paymentInstructions on the internal account to deposit funds.
+ operationId: createTransferIn
tags:
- - Customers
+ - Same-Currency Transfers
security:
- BasicAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
- multipart/form-data:
+ application/json:
schema:
- type: object
- required:
- - file
- properties:
- file:
- type: string
- format: binary
- description: CSV file containing customer information
+ $ref: '#/components/schemas/TransferInRequest'
+ examples:
+ transferIn:
+ summary: Transfer from external to internal account
+ value:
+ source:
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ amount: 12550
responses:
- '202':
- description: CSV upload accepted for processing
+ '201':
+ description: Transfer-in request created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/BulkCustomerImportJobAccepted'
+ $ref: '#/components/schemas/TransactionOneOf'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer or account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /customers/bulk/jobs/{jobId}:
- get:
- summary: Get bulk import job status
+ /transfer-out:
+ post:
+ summary: Create a transfer-out request
description: |
- Retrieve the current status and results of a bulk customer import job. This endpoint can be used
- to track the progress of both CSV uploads.
-
- The response includes:
- - Overall job status
- - Progress statistics
- - Detailed error information for failed entries
- - Completion timestamp when finished
- operationId: getBulkCustomerImportJob
+ Transfer funds from an internal account to an external account for a specific customer.
+ operationId: createTransferOut
tags:
- - Customers
+ - Same-Currency Transfers
security:
- BasicAuth: []
parameters:
- - name: jobId
- in: path
- description: ID of the bulk import job to retrieve
- required: true
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
schema:
type: string
- responses:
- '200':
- description: Job status retrieved successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/BulkCustomerImportJob'
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Job not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /invitations:
- post:
- summary: Create an UMA invitation
- description: |
- Create an UMA invitation from a given platform customer.
- operationId: createInvitation
- tags:
- - Invitations
- security:
- - BasicAuth: []
+ example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/UmaInvitationCreateRequest'
+ $ref: '#/components/schemas/TransferOutRequest'
+ examples:
+ transferOut:
+ summary: Transfer from internal to external account
+ value:
+ source:
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ destination:
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ paymentRail: ACH
+ amount: 12550
+ remittanceInformation: '12345'
responses:
'201':
- description: Invitation created successfully
+ description: |
+ Transfer-out request created successfully.
content:
application/json:
schema:
- $ref: '#/components/schemas/UmaInvitation'
+ $ref: '#/components/schemas/TransactionOneOf'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -3658,36 +3356,61 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Customer or account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /invitations/{invitationCode}:
+ /receiver/uma/{receiverUmaAddress}:
get:
- summary: Get an UMA invitation by code
+ summary: Look up an UMA address for payment
description: |
- Retrieve details about an UMA invitation by its invitation code.
- operationId: getInvitation
+ Lookup a receiving UMA address to determine supported currencies and exchange rates.
+ This endpoint helps platforms determine what currencies they can send to a given UMA address.
+ operationId: lookupUma
tags:
- - Invitations
+ - Cross-Currency Transfers
security:
- BasicAuth: []
parameters:
- - name: invitationCode
+ - name: receiverUmaAddress
in: path
- description: The code of the invitation to get
+ description: UMA address of the intended recipient
required: true
schema:
type: string
+ - name: senderUmaAddress
+ in: query
+ description: UMA address of the sender (optional if customerId is provided)
+ required: false
+ schema:
+ type: string
+ - name: customerId
+ in: query
+ description: System ID of the sender (optional if senderUmaAddress is provided)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Invitation retrieved successfully
+ description: Successful lookup
content:
application/json:
schema:
- $ref: '#/components/schemas/UmaInvitation'
+ $ref: '#/components/schemas/ReceiverUmaLookupResponse'
+ '400':
+ description: Bad request - Missing or invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -3695,56 +3418,69 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Invitation not found
+ description: UMA address not found. The address is well-formed but no receiver exists at the counterparty VASP (`UMA_NOT_FOUND`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '412':
+ description: Counterparty doesn't support UMA version
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error412'
+ '424':
+ description: Counterparty issue
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /invitations/{invitationCode}/claim:
- post:
- summary: Claim an UMA invitation
+ /receiver/external-account/{accountId}:
+ get:
+ summary: Look up an external account for payment
description: |
- Claim an UMA invitation by associating it with an invitee UMA address.
-
- When an invitation is successfully claimed:
- 1. The invitation status changes from PENDING to CLAIMED
- 2. The invitee UMA address is associated with the invitation
- 3. An INVITATION_CLAIMED webhook is triggered to notify the platform that created the invitation
-
- This endpoint allows customers to accept invitations sent to them by other UMA customers.
- operationId: claimInvitation
+ Lookup an external account by ID to determine supported currencies and exchange rates.
+ This endpoint helps platforms determine what currencies they can send to a given external account, along with the current estimated exchange rates and minimum and maximum amounts that can be sent.
+ operationId: lookupExternalAccount
tags:
- - Invitations
+ - Cross-Currency Transfers
security:
- BasicAuth: []
parameters:
- - name: invitationCode
+ - name: accountId
in: path
- description: The code of the invitation to claim
+ description: System-generated ID of the external account
required: true
schema:
type: string
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UmaInvitationClaimRequest'
+ example: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ - name: senderUmaAddress
+ in: query
+ description: UMA address of the sender (optional if customerId is provided)
+ required: false
+ schema:
+ type: string
+ - name: customerId
+ in: query
+ description: System ID of the sender (optional if senderUmaAddress is provided)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Invitation claimed successfully
+ description: Successful lookup
content:
application/json:
schema:
- $ref: '#/components/schemas/UmaInvitation'
+ $ref: '#/components/schemas/ReceiverExternalAccountLookupResponse'
'400':
- description: Bad request
+ description: Bad request - Missing or invalid parameters
content:
application/json:
schema:
@@ -3756,69 +3492,63 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Invitation not found
+ description: External account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '412':
+ description: Counterparty doesn't support UMA version
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error412'
+ '424':
+ description: Counterparty issue
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /invitations/{invitationCode}/cancel:
- post:
- summary: Cancel an UMA invitation
+ /quotes/{quoteId}:
+ get:
+ summary: Get quote by ID
description: |
- Cancel a pending UMA invitation. Only the inviter or platform can cancel an invitation.
-
- When an invitation is cancelled:
- 1. The invitation status changes from PENDING to CANCELLED
- 2. The invitation can no longer be claimed
- 3. The invitation URL will show as cancelled when accessed
-
- Only pending invitations can be cancelled. Attempting to cancel an invitation
- that is already claimed, expired, or cancelled will result in an error.
- operationId: cancelInvitation
+ Retrieve a quote by its ID. If the quote has been settled, it will include
+ the transaction ID. This allows clients to track the full lifecycle of a payment
+ from quote creation to settlement.
+ operationId: getQuoteById
tags:
- - Invitations
+ - Cross-Currency Transfers
security:
- BasicAuth: []
parameters:
- - name: invitationCode
+ - name: quoteId
in: path
- description: The code of the invitation to cancel
+ description: ID of the quote to retrieve
required: true
schema:
type: string
responses:
'200':
- description: Invitation cancelled successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UmaInvitation'
- '400':
- description: Bad request - Invitation cannot be cancelled (already claimed, expired, or cancelled)
+ description: Quote retrieved successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/Quote'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Only the platform which created the invitation can cancel it
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Invitation not found
+ description: Quote not found
content:
application/json:
schema:
@@ -3829,32 +3559,132 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/send:
+ /quotes:
post:
- summary: Simulate sending funds
+ summary: Create a transfer quote
description: |
- Simulate sending funds to the bank account as instructed in the quote.
- This endpoint is only for the sandbox environment and will fail for production platforms/keys.
- operationId: sandboxSend
- tags:
- - Sandbox
- security:
- - BasicAuth: []
+ Generate a quote for a cross-currency transfer between any combination of accounts
+ and UMA addresses. This endpoint handles currency exchange and provides the necessary
+ instructions to execute the transfer.
+
+ **Transfer Types Supported:**
+ - **Account to Account**: Transfer between internal/external accounts with currency exchange.
+ - **Account to UMA**: Transfer from an internal account to an UMA address.
+ - **UMA to Account or UMA to UMA**: This transfer type will only be funded by payment instructions, not from an internal account.
+
+ **Key Features:**
+ - **Flexible Amount Locking**: Always specify whether you want to lock the sending amount or receiving amount
+ - **Currency Exchange**: Handles all cross-currency transfers with real-time exchange rates
+ - **Payment Instructions**: For UMA or customer ID sources, provides banking details needed for execution
+
+ **Important:** If you are transferring funds in the same currency (no exchange required),
+ use the `/transfer-in` or `/transfer-out` endpoints instead.
+ operationId: createQuote
+ tags:
+ - Cross-Currency Transfers
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example:
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/SandboxSendRequest'
+ $ref: '#/components/schemas/QuoteRequest'
+ examples:
+ accountToAccount:
+ summary: Account to Account Transfer
+ value:
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ lockedCurrencySide: SENDING
+ lockedCurrencyAmount: 10000
+ description: Transfer between accounts, either internal or external.
+ accountToUma:
+ summary: Account to UMA Address Transfer
+ value:
+ lookupId: LookupRequest:019542f5-b3e7-1d02-0000-000000000009
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: UMA_ADDRESS
+ umaAddress: $receiver@uma.domain.com
+ currency: EUR
+ lockedCurrencySide: SENDING
+ lockedCurrencyAmount: 1000
+ description: 'Payment for invoice #1234'
+ realTimeFundingToSparkWallet:
+ summary: Real-time funding to Spark Wallet as an on-ramp flow. Immediate execution.
+ value:
+ source:
+ sourceType: REALTIME_FUNDING
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000009
+ currency: USD
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:b23dcbd6-dced-4ec4-b756-3c3a9ea3d456
+ lockedCurrencySide: RECEIVING
+ lockedCurrencyAmount: 10000
+ immediatelyExecute: true
+ description: Bitcoin reward payout!
responses:
- '200':
- description: Funds received successfully
+ '201':
+ description: |
+ Transfer quote created successfully. The response includes exchange rates,
+ fees, and transfer details. For transfers involving UMA addresses, payment
+ instructions are also included for execution through banking systems.
content:
application/json:
schema:
- $ref: '#/components/schemas/OutgoingTransaction'
+ $ref: '#/components/schemas/Quote'
+ example:
+ id: Quote:019542f5-b3e7-1d02-0000-000000000006
+ status: PENDING
+ createdAt: '2025-10-03T12:00:00Z'
+ expiresAt: '2025-10-03T12:05:00Z'
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ sendingCurrency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ receivingCurrency:
+ code: EUR
+ name: Euro
+ symbol: €
+ decimals: 2
+ totalSendingAmount: 10000
+ totalReceivingAmount: 9200
+ exchangeRate: 0.92
+ feesIncluded: 10
+ transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ '202':
+ description: |
+ Quote created but awaiting Strong Customer Authentication. Returned only for customers whose provider requires SCA (e.g. EU) on a realtime-funding source: the quote has status `PENDING_AUTHORIZATION` and carries an `scaChallenge`, and `paymentInstructions` are **withheld** until the challenge is authorized via `POST /quotes/{quoteId}/authorize`. Once authorized, the quote's `paymentInstructions` are populated.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Quote'
'400':
- description: Bad request
+ description: Bad request - Missing or invalid parameters
content:
application/json:
schema:
@@ -3865,121 +3695,172 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
+ '412':
+ description: Counterparty doesn't support UMA version
content:
application/json:
schema:
- $ref: '#/components/schemas/Error403'
- '404':
- description: Quote not found
+ $ref: '#/components/schemas/Error412'
+ '424':
+ description: Counterparty issue
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error424'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/uma/receive:
+ '501':
+ description: Not implemented
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ /quotes/{quoteId}/execute:
post:
- summary: Simulate payment send to test receiving an UMA payment
+ summary: Execute a quote
description: |
- Simulate sending payment from an sandbox uma address to a platform customer to test payment receive.
- This endpoint is only for the sandbox environment and will fail for production platforms/keys.
- operationId: sandboxReceive
+ Execute a quote by its ID. This endpoint initiates the transfer between
+ the source and destination accounts.
+
+ This endpoint can only be used for quotes with a `source` which is either an internal account,
+ or has direct pull functionality (e.g. ACH pull with an external account).
+
+ When the quote's `source` is an internal account of type `EMBEDDED_WALLET`,
+ the request must include a `Grid-Wallet-Signature` header. The header value
+ is the full Grid wallet signature built over the `payloadToSign` value from
+ the quote's `paymentInstructions[].accountOrWalletInfo` entry with the
+ session private key of a verified authentication credential on the source
+ Embedded Wallet.
+
+ Once executed, the quote cannot be cancelled and the transfer will be processed.
+ operationId: executeQuote
tags:
- - Sandbox
+ - Cross-Currency Transfers
security:
- BasicAuth: []
+ parameters:
+ - name: quoteId
+ in: path
+ required: true
+ description: The unique identifier of the quote to execute
+ schema:
+ type: string
+ example: Quote:019542f5-b3e7-1d02-0000-000000000001
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/SandboxUmaReceiveRequest'
+ $ref: '#/components/schemas/ExecuteQuoteRequest'
responses:
'200':
- description: Payment triggered successfully
+ description: |
+ Quote processed. The outcome depends on whether SCA applies to the customer (currently EU customers):
+
+ - **No SCA required:** the transfer is initiated and the quote status advances (`PROCESSING` / `COMPLETED`).
+
+ - **SCA required:** the transfer is **not** initiated yet. The quote is returned with status `PENDING_AUTHORIZATION` and an `scaChallenge`; release the transfer by authorizing the quote you already hold — `POST /quotes/{quoteId}/authorize` (re-calling `execute` returns 409). A pre-funded send is a single challenge, so one authorization releases it. The challenge (and its SMS code / passkey assertion) only comes into existence once this call initiates the transfer, so the proof is always supplied on the follow-up authorize, never inline on this call. If an SMS code lapses, re-send it via `POST /quotes/{quoteId}/authorize/resend`.
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingTransaction'
+ $ref: '#/components/schemas/Quote'
'400':
- description: Bad request
+ description: Bad request - Invalid quote ID or quote cannot be confirmed
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
+ '404':
+ description: Quote not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error403'
- '404':
- description: Sender or receiver not found
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: Conflict - Quote already confirmed, expired, or in invalid state
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/internal-accounts/{accountId}/fund:
+ /quotes/{quoteId}/authorize:
+ parameters:
+ - name: quoteId
+ in: path
+ description: The unique identifier of the quote whose SCA challenge is being authorized.
+ required: true
+ schema:
+ type: string
+ example: Quote:019542f5-b3e7-1d02-0000-000000000006
post:
- summary: Simulate funding an internal account
+ summary: Authorize a quote's SCA challenge
description: |
- Simulate receiving funds into an internal account in the sandbox environment. This is useful for testing scenarios where you need to add funds to a customer's or platform's internal account without going through a real bank transfer or following payment instructions.
- This endpoint is only for the sandbox environment and will fail for production platforms/keys.
- operationId: sandboxFundInternalAccount
+ Satisfy the Strong Customer Authentication challenge carried by a quote in
+ `PENDING_AUTHORIZATION` status by submitting an `ScaAuthorization` proof.
+
+ This is used for realtime-funding quotes: the quote is returned with an
+ `scaChallenge` and **without** `paymentInstructions`; once authorized, the
+ quote advances and its `paymentInstructions` are populated so the customer
+ can fund the transfer.
+
+ As with all SCA, a quote may require more than one authorization: after
+ authorizing, if the quote is still `PENDING_AUTHORIZATION` it carries the
+ next `scaChallenge` — authorize that too, repeating until it advances (see
+ `ScaChallenge`).
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS code is always `123456`.
+ operationId: authorizeQuote
tags:
- - Sandbox
+ - Strong Customer Authentication
security:
- BasicAuth: []
- parameters:
- - name: accountId
- in: path
- required: true
- description: The ID of the internal account to fund
- schema:
- type: string
- example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/SandboxFundRequest'
- examples:
- fundUSDAccount:
- summary: Fund USD internal account with $1,000
- value:
- amount: 100000
- fundBTCAccount:
- summary: Fund BTC internal account with 0.01 BTC
- value:
- amount: 1000000
+ $ref: '#/components/schemas/ScaAuthorization'
responses:
'200':
- description: Internal account funded successfully
+ description: Challenge authorized; the updated quote is returned.
content:
application/json:
schema:
- $ref: '#/components/schemas/InternalAccount'
+ $ref: '#/components/schemas/Quote'
'400':
- description: Bad request
+ description: Invalid or expired authorization proof
content:
application/json:
schema:
@@ -3990,180 +3871,166 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
+ '404':
+ description: Quote not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error403'
- '404':
- description: Internal account not found
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer, or the quote has no pending challenge to authorize.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error409'
+ '429':
+ description: Too many requests. Returned with `RATE_LIMITED` when authorization attempts for this challenge happen too frequently (for example, repeated bad codes brute-forcing the OTP). The challenge may be invalidated after too many failed attempts. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /uma-providers:
- get:
- summary: List available Counterparty Providers
+ /quotes/{quoteId}/authorize/resend:
+ parameters:
+ - name: quoteId
+ in: path
+ description: The unique identifier of the quote whose SCA challenge code should be re-sent.
+ required: true
+ schema:
+ type: string
+ example: Quote:019542f5-b3e7-1d02-0000-000000000006
+ post:
+ summary: Resend a quote's SCA challenge code
description: |
- Retrieve a list of available Counterparty Providers. The response includes basic information about each provider, such as its UMA address, name, and supported currencies.
- operationId: getAvailableUmaProviders
+ Re-send the one-time code for a realtime-funding quote in
+ `PENDING_AUTHORIZATION` status whose `scaChallenge.factor` is `SMS_OTP`. The
+ existing challenge is reused — no new challenge is issued, and its
+ `scaChallenge.expiresAt` is **not** extended; once the challenge is past
+ `expiresAt` it can no longer be authorized.
+
+ Only meaningful for customers in a region where SCA is required (e.g. EU);
+ a 409 is returned otherwise. `PASSKEY` challenges cannot be re-sent and return 409.
+
+ In sandbox, the code is always `123456`.
+ operationId: resendQuoteScaCode
tags:
- - Available UMA Providers
- parameters:
- - in: query
- name: countryCode
- description: The alpha-2 representation of a country, as defined by the ISO 3166-1 standard.
- required: false
- schema:
- type: string
- example: US
- - in: query
- name: currencyCode
- description: The ISO 4217 currency code to filter providers by supported currency.
- required: false
- schema:
- type: string
- example: USD
- - in: query
- name: hasBlockedProviders
- description: Whether to include providers which are not on your allowlist in the response. By default the response will include blocked providers.
- required: false
- schema:
- type: boolean
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
- - name: sortOrder
- in: query
- description: Order to sort results in
- required: false
- schema:
- type: string
- enum:
- - asc
- - desc
- default: desc
+ - Strong Customer Authentication
security:
- BasicAuth: []
responses:
- '200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/UmaProviderListResponse'
+ '204':
+ description: Code re-sent.
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /tokens:
- post:
- summary: Create a new API token
- description: Create a new API token to access the Grid APIs.
- operationId: createToken
- tags:
- - API Tokens
- security:
- - BasicAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ApiTokenCreateRequest'
- responses:
- '201':
- description: API token created successfully
+ '404':
+ description: Quote not found
content:
application/json:
schema:
- $ref: '#/components/schemas/ApiToken'
- '400':
- description: Bad request
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: SCA is not required for this customer, the quote has no pending SMS challenge, or the challenge uses a factor whose code cannot be re-sent (e.g. passkey).
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
+ $ref: '#/components/schemas/Error409'
+ '429':
+ description: Too many requests. Returned with `RATE_LIMITED` when codes are re-sent too frequently, to avoid spamming the customer's phone. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /transactions:
get:
- summary: List tokens
+ summary: List transactions
description: |
- Retrieve a list of API tokens with optional filtering parameters. Returns all tokens that match
- the specified filters. If no filters are provided, returns all tokens (paginated).
- operationId: listTokens
+ Retrieve a paginated list of transactions with optional filtering.
+ The transactions can be filtered by customer ID, platform customer ID, UMA address,
+ date range, status, and transaction type.
+
+ Card transactions are included and identified by `type: CARD`. In Sandbox this is how
+ you discover a `CardTransaction` id after simulating an authorization — list the
+ transactions, take the card transaction's `id`, and pass it as the `cardTransactionId`
+ to the clearing and return simulate endpoints.
+ operationId: listTransactions
tags:
- - API Tokens
+ - Transactions
security:
- BasicAuth: []
parameters:
- - name: name
+ - name: customerId
in: query
- description: Filter by name of the token
+ description: Filter by system customer ID. To filter to transactions made on behalf of the platform, specify the platform ID as the customer ID.
required: false
schema:
type: string
- - name: createdAfter
+ - name: platformCustomerId
in: query
- description: Filter customers created after this timestamp (inclusive)
+ description: Filter by platform-specific customer ID
required: false
schema:
type: string
- format: date-time
- - name: createdBefore
+ - name: accountIdentifier
in: query
- description: Filter customers created before this timestamp (inclusive)
+ description: Filter by account identifier (matches either sender or receiver)
required: false
schema:
type: string
- format: date-time
- - name: updatedAfter
+ - name: senderAccountIdentifier
in: query
- description: Filter customers updated after this timestamp (inclusive)
+ description: Filter by sender account identifier
+ required: false
+ schema:
+ type: string
+ - name: receiverAccountIdentifier
+ in: query
+ description: Filter by receiver account identifier
+ required: false
+ schema:
+ type: string
+ - name: status
+ in: query
+ description: Filter by transaction status
+ required: false
+ schema:
+ $ref: '#/components/schemas/TransactionStatus'
+ - name: type
+ in: query
+ description: Filter by transaction type
+ required: false
+ schema:
+ $ref: '#/components/schemas/TransactionType'
+ - name: reference
+ in: query
+ description: Filter by reference
+ required: false
+ schema:
+ type: string
+ - name: startDate
+ in: query
+ description: Filter by start date (inclusive) in ISO 8601 format
required: false
schema:
type: string
format: date-time
- - name: updatedBefore
+ - name: endDate
in: query
- description: Filter customers updated before this timestamp (inclusive)
+ description: Filter by end date (inclusive) in ISO 8601 format
required: false
schema:
type: string
@@ -4183,13 +4050,23 @@ paths:
required: false
schema:
type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/TokenListResponse'
+ $ref: '#/components/schemas/TransactionListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -4208,20 +4085,20 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /tokens/{tokenId}:
+ /transactions/{transactionId}:
parameters:
- - name: tokenId
+ - name: transactionId
in: path
- description: System-generated unique token identifier
+ description: Unique identifier of the transaction
required: true
schema:
type: string
get:
- summary: Get API token by ID
- description: Retrieve an API token by their system-generated ID
- operationId: getTokenById
+ summary: Get transaction by ID
+ description: Retrieve detailed information about a specific transaction.
+ operationId: getTransactionById
tags:
- - API Tokens
+ - Transactions
security:
- BasicAuth: []
responses:
@@ -4230,7 +4107,7 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/ApiToken'
+ $ref: '#/components/schemas/TransactionOneOf'
'401':
description: Unauthorized
content:
@@ -4238,7 +4115,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Token not found
+ description: Transaction not found
content:
application/json:
schema:
@@ -4249,19 +4126,38 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- delete:
- summary: Delete API token by ID
- description: Delete an API token by their system-generated ID
- operationId: deleteTokenById
+ /transactions/{transactionId}/confirm:
+ post:
+ summary: Confirm receipt delivery
+ description: |
+ Confirm that the platform delivered the transaction receipt to its customer. This confirmation is only necessary when the platform is contractually required to send a receipt. If `receiptDeliveryConfirmedAt` is omitted, the confirmation time is set to the current server time. Calling this endpoint again for the same transaction updates the stored confirmation time.
+ operationId: confirmReceiptDelivery
tags:
- - API Tokens
+ - Transactions
security:
- BasicAuth: []
+ parameters:
+ - name: transactionId
+ in: path
+ description: Unique identifier of the transaction to confirm receipt delivery for
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ConfirmReceiptDeliveryRequest'
responses:
- '204':
- description: API token deleted successfully
+ '200':
+ description: Receipt delivery confirmation recorded successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TransactionOneOf'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
@@ -4273,7 +4169,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Token not found
+ description: Transaction not found
content:
application/json:
schema:
@@ -4284,368 +4180,168 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /internal-accounts/{id}:
- parameters:
- - name: id
- in: path
- description: The id of the internal account to update.
- required: true
- schema:
- type: string
- example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- patch:
- summary: Update internal account
+ /transactions/{transactionId}/approve:
+ post:
+ summary: Approve a pending incoming payment
description: |
- Update mutable fields on an internal account. Today this supports updating the wallet privacy setting for an Embedded Wallet internal account.
-
- Updating wallet privacy is a two-step signed-retry flow:
-
- 1. Call `PATCH /internal-accounts/{id}` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
-
- 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same update fields submitted in step 1. The signed retry returns `200` with the updated internal account.
- operationId: updateInternalAccount
+ Approve a pending incoming payment that was previously acknowledged with a 202 response.
+ This endpoint allows platforms to asynchronously approve payments after async processing.
+ operationId: approvePendingPayment
tags:
- - Internal Accounts
+ - Transactions
security:
- BasicAuth: []
parameters:
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ - name: transactionId
+ in: path
+ description: Unique identifier of the transaction to approve
+ required: true
schema:
type: string
- example: Request:019542f5-b3e7-1d02-0000-000000000010
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/InternalAccountUpdateRequest'
- examples:
- updateWalletPrivacy:
- summary: Update wallet privacy request (both steps)
- value:
- privateEnabled: true
+ $ref: '#/components/schemas/ApprovePaymentRequest'
responses:
'200':
- description: Signed retry accepted. Returns the updated internal account.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/InternalAccount'
- examples:
- enabled:
- summary: Wallet privacy enabled
- value:
- id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- type: EMBEDDED_WALLET
- status: ACTIVE
- balance:
- amount: 12550
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- totalBalance:
- amount: 12550
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- fundingPaymentInstructions: []
- privateEnabled: true
- createdAt: '2026-04-08T15:30:00Z'
- updatedAt: '2026-04-08T15:35:02Z'
- '202':
- description: Challenge issued. The response contains `payloadToSign` (which binds the submitted update fields) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry.
+ description: Payment approved successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/SignedRequestChallenge'
- examples:
- challenge:
- summary: Internal account update challenge
- value:
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"encoding":"PAYLOAD_ENCODING_HEXADECIMAL","hashFunction":"HASH_FUNCTION_NO_OP","payload":"9f3b...","signWith":"sp1q..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2"}'
- requestId: Request:019542f5-b3e7-1d02-0000-000000000010
- expiresAt: '2026-04-08T15:35:00Z'
+ $ref: '#/components/schemas/IncomingTransaction'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters or payment cannot be approved
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending internal account update challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry body does not match the update fields bound into `payloadToSign` on the initial call.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Internal account not found
+ description: Transaction not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: Conflict - Payment is not in a pending state or has already been processed or timed out.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /internal-accounts/{id}/export:
+ /transactions/{transactionId}/reject:
post:
- summary: Export internal account wallet credentials
+ summary: Reject a pending incoming payment
description: |
- Export the wallet credentials of an Embedded Wallet internal account. The returned wallet credentials are HPKE-encrypted to the `clientPublicKey` supplied in the request body.
-
- Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session):
-
- 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`.
-
- 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key.
-
- The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against.
- operationId: exportInternalAccount
+ Reject a pending incoming payment that was previously acknowledged with a 202 response.
+ This endpoint allows platforms to asynchronously reject payments after additional processing.
+ operationId: rejectPendingPayment
tags:
- - Internal Accounts
+ - Transactions
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: transactionId
in: path
- description: The id of the internal account to export.
+ description: Unique identifier of the transaction to reject
required: true
schema:
type: string
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/InternalAccountExportRequest'
- examples:
- export:
- summary: Export request (both steps)
- value:
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ $ref: '#/components/schemas/RejectPaymentRequest'
responses:
'200':
- description: Signed retry accepted. Returns the encrypted wallet credentials.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/InternalAccountExportResponse'
- '202':
- description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry.
+ description: Payment rejected successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/SignedRequestChallenge'
+ $ref: '#/components/schemas/IncomingTransaction'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters or payment cannot be rejected
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending export challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Internal account not found
+ description: Transaction not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
+ '409':
+ description: Conflict - Payment is not in a pending state or has already been processed or timed out.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/credentials:
+ /crypto/estimate-withdrawal-fee:
post:
- summary: Create an authentication credential
+ summary: Estimate crypto withdrawal fee
description: |
- Register an authentication credential for an Embedded Wallet customer.
-
- Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered.
-
- Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.
- operationId: createAuthCredential
+ Estimate the network and application fees for a cryptocurrency withdrawal from a crypto internal account to an external blockchain address. Use this to show fee information to customers before they initiate a withdrawal.
+ operationId: estimateCryptoWithdrawalFee
tags:
- - Embedded Wallet Auth
+ - Cross-Currency Transfers
security:
- BasicAuth: []
- parameters:
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required on the signed retry.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering a credential; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf'
+ $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeRequest'
examples:
- emailOtp:
- summary: Add an email OTP credential
- value:
- type: EMAIL_OTP
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- smsOtp:
- summary: Add an SMS OTP credential
- value:
- type: SMS_OTP
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- oauth:
- summary: Add an OAuth credential
+ estimateUSDCWithdrawal:
+ summary: Estimate fee for USDC withdrawal on Solana
value:
- type: OAUTH
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
- passkey:
- summary: Add a passkey credential
- value:
- type: PASSKEY
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- nickname: iPhone Face-ID
- challenge: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx
- attestation:
- credentialId: AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY
- clientDataJson: eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0
- attestationObject: o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow
- transports:
- - internal
- - hybrid
+ internalAccountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ currency: USDC
+ cryptoNetwork: SOLANA
+ amount: 1000000
+ destinationAddress: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU
responses:
- '201':
- description: Authentication credential created successfully. The body is the created `AuthMethod`. For `EMAIL_OTP`, the nickname is the customer email tied to the internal account; for `SMS_OTP`, it is the customer phone number. OTP responses that trigger a secure OTP challenge carry `otpEncryptionTargetBundle` — the HPKE target bundle the client uses to encrypt the OTP attempt on the subsequent `POST /auth/credentials/{id}/verify`. First-time EMAIL_OTP wallet bootstrap responses may omit that bundle; if it is absent, call `POST /auth/credentials/{id}/challenge` for the new credential to issue a fresh OTP and receive `otpEncryptionTargetBundle` before verifying. For `PASSKEY`, the credential must be authenticated for the first time via `POST /auth/credentials/{id}/challenge` followed by `POST /auth/credentials/{id}/verify` to produce a session — there is no inline authentication challenge on the registration response.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthMethodResponse'
- examples:
- emailOtp:
- summary: Email OTP credential created
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: EMAIL_OTP
- nickname: example@lightspark.com
- otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- smsOtp:
- summary: SMS OTP credential created
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: SMS_OTP
- nickname: '+14155550123'
- otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- oauth:
- summary: OAuth credential created
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: OAUTH
- nickname: example@lightspark.com
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- passkey:
- summary: Passkey credential created
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: PASSKEY
- nickname: iPhone Face-ID
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- '202':
- description: Challenge issued. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry.
+ '200':
+ description: Fee estimation returned successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthSignedRequestChallenge'
- examples:
- emailOtp:
- summary: Additional email OTP credential challenge
- value:
- type: EMAIL_OTP
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userEmail":"jane@example.com","userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_EMAIL"}'
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
- smsOtp:
- summary: Additional SMS OTP credential challenge
- value:
- type: SMS_OTP
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F...","userPhoneNumber":"+14155550123"},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER"}'
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
- oauth:
- summary: Additional OAuth credential challenge
- value:
- type: OAUTH
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"oauthProviders":[{"oidcToken":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...","providerName":"Google"}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS"}'
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
- passkey:
- summary: Additional passkey credential challenge
- value:
- type: PASSKEY
- payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"authenticators":[{"attestation":{"attestationObject":"o2NmbXRk...","clientDataJson":"eyJjaGFsbGVuZ2UiOiJBcktRa...","credentialId":"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY"},"authenticatorName":"iPhone Face-ID","challenge":"ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx","transports":["internal","hybrid"]}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2"}'
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
+ $ref: '#/components/schemas/EstimateCryptoWithdrawalFeeResponse'
'400':
- description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `SMS_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an SMS OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer.
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration.
+ description: Unauthorized
content:
application/json:
schema:
@@ -4662,62 +4358,24 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- get:
- summary: List authentication credentials
- description: |-
- Retrieve all authentication credentials registered on an Embedded Wallet internal account.
-
- The response is not paginated: an internal account is expected to have a small, bounded number of credentials (typically 1–5), so all results are returned inline. Additional per-credential detail (such as active session expiry) is available on `GET /auth/sessions`.
- operationId: listAuthCredentials
+ /sandbox/webhooks/test:
+ post:
+ summary: Send a test webhook
+ description: Send a test webhook to the configured endpoint
+ operationId: sendTestWebhook
tags:
- - Embedded Wallet Auth
+ - Sandbox
security:
- BasicAuth: []
- parameters:
- - name: accountId
- in: query
- description: Internal account id whose authentication credentials to list.
- required: true
- schema:
- type: string
- example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
responses:
'200':
- description: Authentication credentials registered on the internal account. Returns an empty `data` array when the internal account has no credentials or when `accountId` does not match any internal account visible to the caller.
+ description: Webhook delivered successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthCredentialListResponse'
- examples:
- multipleCredentials:
- summary: Internal account with multiple authentication credentials
- value:
- data:
- - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: EMAIL_OTP
- nickname: example@lightspark.com
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:01Z'
- - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000004
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: OAUTH
- nickname: example@lightspark.com
- createdAt: '2026-04-08T15:35:00Z'
- updatedAt: '2026-04-08T15:35:00Z'
- - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000003
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: PASSKEY
- credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
- nickname: iPhone Face-ID
- createdAt: '2026-04-09T10:15:00Z'
- updatedAt: '2026-04-09T10:15:00Z'
- empty:
- summary: No credentials registered on the account
- value:
- data: []
+ $ref: '#/components/schemas/TestWebhookResponse'
'400':
- description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier.
+ description: Bad request - Webhook delivery failed
content:
application/json:
schema:
@@ -4734,282 +4392,168 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/credentials/{id}:
- delete:
- summary: Revoke an authentication credential
+ /customers/bulk/csv:
+ post:
+ summary: Upload customers via CSV file
description: |
- Revoke an authentication credential on an Embedded Wallet internal account.
+ Upload a CSV file containing customer information for bulk creation. The CSV file should follow
+ a specific format with required and optional columns based on customer type.
- Revocation is a two-step flow because it must be authorized by a session on a *different* credential on the same internal account:
+ ### CSV Format
+ The CSV file should have the following columns:
- 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+ Required columns for all customers:
+ - umaAddress: The customer's UMA address (e.g., $john.doe@uma.domain.com)
+ - platformCustomerId: Your platform's unique identifier for the customer
+ - customerType: Either "INDIVIDUAL" or "BUSINESS"
- 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`.
+ Required columns for individual customers:
+ - fullName: Individual's full name
+ - birthDate: Date of birth in YYYY-MM-DD format
+ - addressLine1: Street address line 1
+ - city: City
+ - state: State/Province/Region
+ - postalCode: Postal/ZIP code
+ - country: Country code (ISO 3166-1 alpha-2)
- The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it.
- operationId: revokeAuthCredential
+ Required columns for business customers:
+ - businessLegalName: Legal name of the business
+ - addressLine1: Street address line 1
+ - city: City
+ - state: State/Province/Region
+ - postalCode: Postal/ZIP code
+ - country: Country code (ISO 3166-1 alpha-2)
+
+ Optional columns for all customers:
+ - addressLine2: Street address line 2
+ - platformAccountId: Your platform's identifier for the bank account
+ - description: Optional description for the customer
+
+ Optional columns for individual customers:
+ - email: Customer's email address
+
+ Optional columns for business customers:
+ - businessRegistrationNumber: Business registration number
+ - businessTaxId: Tax identification number
+
+ ### Example CSV
+ ```csv
+ umaAddress,platformCustomerId,customerType,fullName,birthDate,addressLine1,city,state,postalCode,country,platformAccountId,businessLegalName
+ john.doe@uma.domain.com,customer123,INDIVIDUAL,John Doe,1990-01-15,123 Main St,San Francisco,CA,94105,US
+ acme@uma.domain.com,biz456,BUSINESS,,,400 Commerce Way,Austin,TX,78701,US
+ ```
+
+ The upload process is asynchronous and will return a job ID that can be used to track progress.
+ You can monitor the job status using the `/customers/bulk/jobs/{jobId}` endpoint.
+ operationId: uploadCustomersCsv
tags:
- - Embedded Wallet Auth
+ - Customers
security:
- BasicAuth: []
- parameters:
- - name: id
- in: path
- description: The id of the authentication credential to revoke (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
- required: true
- schema:
- type: string
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required:
+ - file
+ properties:
+ file:
+ type: string
+ format: binary
+ description: CSV file containing customer information
responses:
'202':
- description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSignedRequestChallenge'
- '204':
- description: Authentication credential revoked successfully.
- '400':
- description: Bad request. Also returned when the target internal account has only a single authentication credential, which cannot be revoked via this endpoint.
+ description: CSV upload accepted for processing
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/BulkCustomerImportJobAccepted'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this credential, or when the `Request-Id` does not match an unexpired pending challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Authentication credential not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/credentials/{id}/verify:
- post:
- summary: Verify an authentication credential
+ /customers/bulk/jobs/{jobId}:
+ get:
+ summary: Get bulk import job status
description: |
- Complete the verification step for a previously created authentication credential and issue a session.
-
- For `EMAIL_OTP` and `SMS_OTP` credentials, submit the `encryptedOtpBundle` produced by HPKE-encrypting `{otp_code, public_key}` under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued. The server is a pass-through and never sees the plaintext OTP code. On success the response is `202` with a `payloadToSign` carrying the `verificationToken` bound to the client's TEK public key — sign that token with the matching TEK private key, then retry the same request with the full stamp in `Grid-Wallet-Signature` and the `requestId` echoed in `Request-Id`. The signed retry returns `200` with the issued `AuthSession`. The TEK public key becomes the session API key on successful completion.
- In sandbox mode, the OTP flow runs real HPKE end-to-end against a sandbox enclave keypair — clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code (`"000000"`) the user "receives" instead of a real email or SMS delivery.
-
- For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request.
+ Retrieve the current status and results of a bulk customer import job. This endpoint can be used
+ to track the progress of both CSV uploads.
- On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires.
- operationId: verifyAuthCredential
+ The response includes:
+ - Overall job status
+ - Progress statistics
+ - Detailed error information for failed entries
+ - Completion timestamp when finished
+ operationId: getBulkCustomerImportJob
tags:
- - Embedded Wallet Auth
+ - Customers
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: jobId
in: path
- description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
+ description: ID of the bulk import job to retrieve
required: true
schema:
type: string
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the TEK (Target Encryption Key) keypair the client generated for this login. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification. Not used by `OAUTH` or `PASSKEY` verification, which complete in a single call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response from this endpoint, echoed back exactly here so the server can correlate the signed retry with the issued challenge. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification; must be paired with `Grid-Wallet-Signature`. For `PASSKEY` verification, the `requestId` issued from `POST /auth/credentials/{id}/challenge` is echoed here instead so the server can correlate the assertion with the pending challenge.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf'
- examples:
- emailOtp:
- summary: Verify an email OTP credential (first leg)
- value:
- type: EMAIL_OTP
- encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
- smsOtp:
- summary: Verify an SMS OTP credential (first leg)
- value:
- type: SMS_OTP
- encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
- emailOtpSignedRetry:
- summary: Signed retry completing an email OTP verification
- description: Same request body as the first leg, plus the `Grid-Wallet-Signature` and `Request-Id` headers carrying the stamp over the `verificationToken` and the `requestId` from the prior `202` response.
- value:
- type: EMAIL_OTP
- encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
- oauth:
- summary: Verify an OAuth credential
- value:
- type: OAUTH
- oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
- passkey:
- summary: Verify a passkey credential
- value:
- type: PASSKEY
- assertion:
- credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
- clientDataJson: eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0
- authenticatorData: PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA
- signature: MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6jhd45bDx92wjXKs900
responses:
'200':
- description: Authentication credential verified and session issued
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSession'
- '202':
- description: Verification challenge issued. Returned only for OTP credentials, on the first leg of the secure OTP login flow. Build an API-key stamp over `payloadToSign` (the `verificationToken`) with the TEK keypair the client generated for this login, then resubmit the same request with that full stamp as `Grid-Wallet-Signature` and `requestId` echoed as `Request-Id` to receive the issued session on the signed retry.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSignedRequestChallenge'
- examples:
- emailOtp:
- summary: Email OTP verification challenge (sign and retry)
- value:
- type: EMAIL_OTP
- payloadToSign: eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ.eyJzdWIiOiJUWnk2NkVPa1RGYTd2NkpXZ0VxaVgyZGFXOENXc2pMQzVDVU9aRUlGY3hzIiwiaWF0IjoxNzc5NDA3MjIxLCJleHAiOjE3Nzk0MTA4MjF9.gKX9MWYGkw8Y55bgzsgrRftvUHFruIe8yu0w9Kpjp5qnrZnXcTV71WVoltGPsr015IY_oRTOkIFLHmiGNG9zBw
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
- '400':
- description: Bad request
+ description: Job status retrieved successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/BulkCustomerImportJob'
'401':
- description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP` or `SMS_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. For OTP signed retries, returned when `Grid-Wallet-Signature` is missing, malformed, signed by a public key that does not match the one bound into the `verificationToken`, or when `Request-Id` does not match an unexpired pending verification challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Authentication credential not found
+ description: Job not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '429':
- description: Too many requests. Returned with `RATE_LIMITED` when verification attempts for this credential happen too frequently (for example, repeated bad OTPs or rapid-fire reauthentication retries). Clients should back off and retry after the interval indicated by the `Retry-After` response header.
- headers:
- Retry-After:
- description: Number of seconds to wait before retrying the request.
- schema:
- type: integer
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/credentials/{id}/challenge:
+ /invitations:
post:
- summary: Re-issue an authentication credential challenge
+ summary: Create an UMA invitation
description: |
- Re-issue the challenge for an existing authentication credential.
-
- For `EMAIL_OTP` and `SMS_OTP` credentials, this triggers a new one-time password to the contact on file and returns a fresh `otpEncryptionTargetBundle` for the client to HPKE-encrypt the OTP attempt against. After the user receives the new OTP, build the `encryptedOtpBundle` under the new target bundle and call `POST /auth/credentials/{id}/verify` to begin the secure OTP login flow.
-
- `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`.
-
- For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session.
- operationId: challengeAuthCredential
+ Create an UMA invitation from a given platform customer.
+ operationId: createInvitation
tags:
- - Embedded Wallet Auth
+ - Invitations
security:
- BasicAuth: []
- parameters:
- - name: id
- in: path
- description: The id of the authentication credential to re-challenge (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
- required: true
- schema:
- type: string
requestBody:
- description: Request body. Required when re-challenging a `PASSKEY` credential (must carry `clientPublicKey`). Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient — the OTP is delivered out-of-band. OAuth credentials do not use this endpoint.
- required: false
+ required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthCredentialChallengeRequest'
- examples:
- passkey:
- summary: Re-challenge a passkey credential
- value:
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
- emailOtp:
- summary: Re-challenge an email-OTP credential (empty body)
- value: {}
- smsOtp:
- summary: Re-challenge an SMS-OTP credential (empty body)
- value: {}
+ $ref: '#/components/schemas/UmaInvitationCreateRequest'
responses:
- '200':
- description: Challenge re-issued for the authentication credential. For `EMAIL_OTP` and `SMS_OTP` the body is a plain `AuthMethod` and a new OTP has been sent. For `PASSKEY` the body is a `PasskeyAuthChallenge` carrying the passkey `credentialId`, freshly issued `challenge`, `requestId`, and `expiresAt` required to complete reauthentication via `POST /auth/credentials/{id}/verify`.
+ '201':
+ description: Invitation created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthCredentialResponseOneOf'
- examples:
- emailOtp:
- summary: Email OTP challenge re-issued
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: EMAIL_OTP
- nickname: example@lightspark.com
- otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:35:00Z'
- passkey:
- summary: Passkey reauthentication challenge issued
- value:
- id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: PASSKEY
- credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
- nickname: iPhone Face-ID
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:35:00Z'
- challenge: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729
- requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
- expiresAt: '2026-04-08T15:35:00Z'
+ $ref: '#/components/schemas/UmaInvitation'
'400':
description: Bad request
content:
@@ -5022,120 +4566,91 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Authentication credential not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '429':
- description: Too many requests. Returned with `RATE_LIMITED` when challenge re-issues are requested more frequently than the credential challenge rate limit allows. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
- headers:
- Retry-After:
- description: Number of seconds to wait before retrying the request.
- schema:
- type: integer
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/sessions:
+ /invitations/{invitationCode}:
get:
- summary: List active sessions
- description: |-
- Retrieve all active authentication sessions on an Embedded Wallet internal account. A session is created each time a credential is verified via `POST /auth/credentials/{id}/verify`, and remains active until its `expiresAt` passes or it is revoked via `DELETE /auth/sessions/{id}`.
-
- The response is not paginated: an internal account is expected to have a small, bounded number of concurrent sessions (one per signed-in device, typically 1–4), so all results are returned inline.
- operationId: listAuthSessions
+ summary: Get an UMA invitation by code
+ description: |
+ Retrieve details about an UMA invitation by its invitation code.
+ operationId: getInvitation
tags:
- - Embedded Wallet Auth
+ - Invitations
security:
- BasicAuth: []
parameters:
- - name: accountId
- in: query
- description: Internal account id whose sessions to list.
+ - name: invitationCode
+ in: path
+ description: The code of the invitation to get
required: true
schema:
type: string
- example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
responses:
'200':
- description: Active authentication sessions on the internal account. Returns an empty `data` array when the internal account has no active sessions or when `accountId` does not match any internal account visible to the caller.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SessionListResponse'
- '400':
- description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier.
+ description: Invitation retrieved successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/UmaInvitation'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
+ '404':
+ description: Invitation not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/sessions/{id}:
- delete:
- summary: Revoke an authentication session
+ /invitations/{invitationCode}/claim:
+ post:
+ summary: Claim an UMA invitation
description: |
- Revoke an authentication session on an Embedded Wallet internal account. Revocation is a two-step signed-retry flow:
-
- 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+ Claim an UMA invitation by associating it with an invitee UMA address.
- 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`.
+ When an invitation is successfully claimed:
+ 1. The invitation status changes from PENDING to CLAIMED
+ 2. The invitee UMA address is associated with the invitation
+ 3. An INVITATION_CLAIMED webhook is triggered to notify the platform that created the invitation
- Sessions also expire on their own. `404` is returned whenever the `id` does not match an active session — whether the session was never issued, was already revoked by a prior call, or has expired past its `expiresAt`. The response code reflects the resource state, not an error in the client's flow: re-revoking an already-revoked or expired session is safe and idempotent at the user intent level.
- operationId: revokeAuthSession
+ This endpoint allows customers to accept invitations sent to them by other UMA customers.
+ operationId: claimInvitation
tags:
- - Embedded Wallet Auth
+ - Invitations
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: invitationCode
in: path
- description: The id of the session to revoke.
+ description: The code of the invitation to claim
required: true
schema:
type: string
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UmaInvitationClaimRequest'
responses:
- '202':
- description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry.
+ '200':
+ description: Invitation claimed successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AuthSignedRequestChallenge'
- '204':
- description: Session revoked successfully.
+ $ref: '#/components/schemas/UmaInvitation'
'400':
description: Bad request
content:
@@ -5143,13 +4658,13 @@ paths:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this session, or when the `Request-Id` does not match an unexpired pending challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Session not found
+ description: Invitation not found
content:
application/json:
schema:
@@ -5160,101 +4675,58 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/sessions/{id}/refresh:
+ /invitations/{invitationCode}/cancel:
post:
- summary: Refresh an authentication session
+ summary: Cancel an UMA invitation
description: |
- Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow:
-
- 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+ Cancel a pending UMA invitation. Only the inviter or platform can cancel an invitation.
- 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key.
+ When an invitation is cancelled:
+ 1. The invitation status changes from PENDING to CANCELLED
+ 2. The invitation can no longer be claimed
+ 3. The invitation URL will show as cancelled when accessed
- The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead.
- operationId: refreshAuthSession
+ Only pending invitations can be cancelled. Attempting to cancel an invitation
+ that is already claimed, expired, or cancelled will result in an error.
+ operationId: cancelInvitation
tags:
- - Embedded Wallet Auth
+ - Invitations
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: invitationCode
in: path
- description: The id of the active session to refresh.
+ description: The code of the invitation to cancel
required: true
schema:
type: string
- example: Session:019542f5-b3e7-1d02-0000-000000000003
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the current session API keypair. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in the prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:019542f5-b3e7-1d02-0000-000000000010
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSessionRefreshRequest'
- examples:
- refresh:
- summary: Refresh an active session
- value:
- clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
responses:
- '201':
- description: New authentication session created successfully.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AuthSession'
- examples:
- session:
- summary: Refreshed authentication session
- value:
- id: Session:019542f5-b3e7-1d02-0000-000000000011
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- type: EMAIL_OTP
- encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
- nickname: example@lightspark.com
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:35:00Z'
- expiresAt: '2026-04-08T15:50:00Z'
- '202':
- description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the current session API keypair, then echo `requestId` on the signed retry.
+ '200':
+ description: Invitation cancelled successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/SignedRequestChallenge'
- examples:
- challenge:
- summary: Session refresh challenge
- value:
- payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}'
- requestId: Request:019542f5-b3e7-1d02-0000-000000000010
- expiresAt: '2026-04-08T15:35:00Z'
+ $ref: '#/components/schemas/UmaInvitation'
'400':
- description: Bad request
+ description: Bad request - Invitation cannot be cancelled (already claimed, expired, or cancelled)
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the `BasicAuth` credentials are missing or invalid, when the target session is no longer active and cannot be used for refresh, when the signed retry omits `Grid-Wallet-Signature`, when the provided signature is malformed or does not match the pending refresh challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - Only the platform which created the invitation can cancel it
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
'404':
- description: Session not found
+ description: Invitation not found
content:
application/json:
schema:
@@ -5265,80 +4737,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/delegated-keys:
+ /sandbox/send:
post:
- summary: Create a delegated signing key
+ summary: Simulate sending funds
description: |
- Delegate Spark token-transaction signing authority for a card funding source backed by an Embedded Wallet internal account to a Grid-custodied P-256 API key. Grid uses the requested card and internal account to identify the wallet funding source, generates the keypair server-side, creates an isolated signer identity holding the public key, then policies granting that identity signing and self-revocation authority. The private key is custodied by Grid and never returned. Both activities must be authorized by the wallet owner, so creation is a three-leg signed-retry flow:
-
- 1. Call `POST /auth/delegated-keys` with no signature headers. Grid generates the delegated keypair and the response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
-
- 2. Use the session API keypair of a verified credential on the requested Embedded Wallet internal account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The response is a second `202` with a new `payloadToSign`, `requestId`, and `expiresAt`.
-
- 3. Stamp the new `payloadToSign` with the same session keypair and retry once more with the new `Request-Id`. The signed retry returns `201` with the created `DelegatedKey` in `ACTIVE` status.
-
- The same request body must be sent on all three legs. A flow abandoned after the second leg leaves the key in `PENDING` status: the signer identity exists but holds no policies, so it cannot sign or revoke itself. Abandoned `PENDING` keys do not block creating another delegated key. After activation, Grid uses the custodied key to authorize signing for the card's Embedded Wallet funding account in place of a session keypair; the platform never handles the key material.
-
- Each card funding source may have at most one `ACTIVE` delegated key for its Embedded Wallet funding account; revoke the existing active key before creating a new one. A delegated key authorizes raw-payload signing for the wallet and cannot be scoped to amounts or recipients by the public API. Revoke it with `DELETE /auth/delegated-keys/{id}` when no longer needed.
- operationId: createDelegatedKey
+ Simulate sending funds to the bank account as instructed in the quote.
+ This endpoint is only for the sandbox environment and will fail for production platforms/keys.
+ operationId: sandboxSend
tags:
- - Embedded Wallet Auth
+ - Sandbox
security:
- BasicAuth: []
- parameters:
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified credential on the same internal account. Required on the signed retries; ignored on the initial call.
- schema:
- type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in the prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retries; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/DelegatedKeyCreateRequest'
- examples:
- create:
- summary: Delegate signing to a Grid-custodied key
- value:
- cardId: Card:019542f5-b3e7-1d02-0000-000000000010
- internalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- nickname: Card payments key
+ $ref: '#/components/schemas/SandboxSendRequest'
responses:
- '201':
- description: Delegated key created and policy granted. The key is `ACTIVE` and Grid may use it to stamp card-payment quote executions for this card funding source's Embedded Wallet funding account.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/DelegatedKey'
- examples:
- delegatedKey:
- summary: Active delegated key
- value:
- id: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
- cardId: Card:019542f5-b3e7-1d02-0000-000000000010
- fundingSourceId: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011
- accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- publicKey: 02a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90
- nickname: Card payments key
- status: ACTIVE
- createdAt: '2026-04-08T15:30:01Z'
- updatedAt: '2026-04-08T15:30:42Z'
- '202':
- description: Challenge issued for the next leg. Stamp `payloadToSign` and retry the same request with `Grid-Wallet-Signature` and `Request-Id`.
+ '200':
+ description: Funds received successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/DelegatedKeySignedRequestChallenge'
+ $ref: '#/components/schemas/OutgoingTransaction'
'400':
description: Bad request
content:
@@ -5346,59 +4768,53 @@ paths:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing on a retry, malformed, or does not match the pending challenge, or when the `Request-Id` does not match an unexpired pending challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Card, card funding source, or Embedded Wallet funding account not found
+ '403':
+ description: Forbidden - request was made with a production platform token
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: An `ACTIVE` delegated key already exists for this card funding source. Revoke it with `DELETE /auth/delegated-keys/{id}` before creating a new one.
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Quote not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- get:
- summary: List delegated signing keys
- description: List delegated signing keys for an Embedded Wallet internal account, a card funding source, or both, including `PENDING` keys (user created but policy leg never completed) and `REVOKED` keys. At least one of `accountId` or `fundingSourceId` must be supplied.
- operationId: listDelegatedKeys
+ /sandbox/uma/receive:
+ post:
+ summary: Simulate payment send to test receiving an UMA payment
+ description: |
+ Simulate sending payment from an sandbox uma address to a platform customer to test payment receive.
+ This endpoint is only for the sandbox environment and will fail for production platforms/keys.
+ operationId: sandboxReceive
tags:
- - Embedded Wallet Auth
+ - Sandbox
security:
- BasicAuth: []
- parameters:
- - name: accountId
- in: query
- required: false
- description: The id of the internal account whose delegated keys to list.
- schema:
- type: string
- example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- - name: fundingSourceId
- in: query
- required: false
- description: The id of the card funding source whose delegated keys to list.
- schema:
- type: string
- example: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxUmaReceiveRequest'
responses:
'200':
- description: Delegated keys matching the supplied filters. Returns an empty `data` array when no matching delegated keys are visible to the caller.
+ description: Payment triggered successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/DelegatedKeyListResponse'
+ $ref: '#/components/schemas/IncomingTransaction'
'400':
description: Bad request
content:
@@ -5411,44 +4827,85 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Sender or receiver not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /auth/delegated-keys/{id}:
- get:
- summary: Get a delegated signing key
- description: Retrieve a delegated signing key by its system-generated id.
- operationId: getDelegatedKeyById
+ /sandbox/internal-accounts/{accountId}/fund:
+ post:
+ summary: Simulate funding an internal account
+ description: |
+ Simulate receiving funds into an internal account in the sandbox environment. This is useful for testing scenarios where you need to add funds to a customer's or platform's internal account without going through a real bank transfer or following payment instructions.
+ This endpoint is only for the sandbox environment and will fail for production platforms/keys.
+ operationId: sandboxFundInternalAccount
tags:
- - Embedded Wallet Auth
+ - Sandbox
security:
- BasicAuth: []
parameters:
- - name: id
+ - name: accountId
in: path
- description: The id of the delegated key to retrieve (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys` or `GET /auth/delegated-keys`).
required: true
+ description: The ID of the internal account to fund
schema:
type: string
- example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
+ example: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxFundRequest'
+ examples:
+ fundUSDAccount:
+ summary: Fund USD internal account with $1,000
+ value:
+ amount: 100000
+ fundBTCAccount:
+ summary: Fund BTC internal account with 0.01 BTC
+ value:
+ amount: 1000000
responses:
'200':
- description: Successful operation
+ description: Internal account funded successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/DelegatedKey'
+ $ref: '#/components/schemas/InternalAccount'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
'404':
- description: Delegated key not found
+ description: Internal account not found
content:
application/json:
schema:
@@ -5459,60 +4916,88 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- delete:
- summary: Revoke a delegated signing key
+ /uma-providers:
+ get:
+ summary: List available Counterparty Providers
description: |
- Revoke an `ACTIVE` delegated signing key. Grid uses the custodied delegated key to authorize deleting its own signer identity. Deleting the identity also removes its API key, after which the delegated key can no longer sign. The response is `204` when revocation completes.
-
- The underlying signing policies are left in place. Their consensus references the now-deleted signer identity, so they can never authorize anything, and deleting them is unnecessary for correctness or security.
- operationId: revokeDelegatedKey
+ Retrieve a list of available Counterparty Providers. The response includes basic information about each provider, such as its UMA address, name, and supported currencies.
+ operationId: getAvailableUmaProviders
tags:
- - Embedded Wallet Auth
- security:
- - BasicAuth: []
+ - Available UMA Providers
parameters:
- - name: id
- in: path
- description: The id of the delegated key to revoke (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys`).
- required: true
+ - in: query
+ name: countryCode
+ description: The alpha-2 representation of a country, as defined by the ISO 3166-1 standard.
+ required: false
schema:
type: string
- example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
+ example: US
+ - in: query
+ name: currencyCode
+ description: The ISO 4217 currency code to filter providers by supported currency.
+ required: false
+ schema:
+ type: string
+ example: USD
+ - in: query
+ name: hasBlockedProviders
+ description: Whether to include providers which are not on your allowlist in the response. By default the response will include blocked providers.
+ required: false
+ schema:
+ type: boolean
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
+ security:
+ - BasicAuth: []
responses:
- '204':
- description: Delegated key revoked. The key can no longer authorize signing.
- '400':
- description: Bad request. Returned when the delegated key has already been revoked or is not `ACTIVE`.
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/UmaProviderListResponse'
'401':
- description: Unauthorized.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Delegated key not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents:
+ /tokens:
post:
- summary: Create an agent
- description: |
- Create a new agent with a specified policy. Returns the created agent and a device code that must be redeemed by the agent software to complete installation.
- operationId: createAgent
+ summary: Create a new API token
+ description: Create a new API token to access the Grid APIs.
+ operationId: createToken
tags:
- - Agent Management
+ - API Tokens
security:
- BasicAuth: []
requestBody:
@@ -5520,14 +5005,14 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentCreateRequest'
+ $ref: '#/components/schemas/ApiTokenCreateRequest'
responses:
'201':
- description: Agent created successfully
+ description: API token created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentCreateResponse'
+ $ref: '#/components/schemas/ApiToken'
'400':
description: Bad request
content:
@@ -5547,56 +5032,46 @@ paths:
schema:
$ref: '#/components/schemas/Error500'
get:
- summary: List agents
- description: Retrieve a paginated list of agents for the authenticated platform.
- operationId: listAgents
+ summary: List tokens
+ description: |
+ Retrieve a list of API tokens with optional filtering parameters. Returns all tokens that match
+ the specified filters. If no filters are provided, returns all tokens (paginated).
+ operationId: listTokens
tags:
- - Agent Management
+ - API Tokens
security:
- BasicAuth: []
parameters:
- - name: customerId
+ - name: name
in: query
- description: Filter by customer ID
+ description: Filter by name of the token
required: false
schema:
type: string
- - name: isPaused
- in: query
- description: Filter by paused status
- required: false
- schema:
- type: boolean
- - name: isConnected
- in: query
- description: Filter by connection status (whether the device code has been redeemed)
- required: false
- schema:
- type: boolean
- name: createdAfter
in: query
- description: Filter agents created after this timestamp (inclusive)
+ description: Filter customers created after this timestamp (inclusive)
required: false
schema:
type: string
format: date-time
- name: createdBefore
in: query
- description: Filter agents created before this timestamp (inclusive)
+ description: Filter customers created before this timestamp (inclusive)
required: false
schema:
type: string
format: date-time
- name: updatedAfter
in: query
- description: Filter agents updated after this timestamp (inclusive)
+ description: Filter customers updated after this timestamp (inclusive)
required: false
schema:
type: string
format: date-time
- name: updatedBefore
in: query
- description: Filter agents updated before this timestamp (inclusive)
+ description: Filter customers updated before this timestamp (inclusive)
required: false
schema:
type: string
@@ -5622,7 +5097,7 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentListResponse'
+ $ref: '#/components/schemas/TokenListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -5641,153 +5116,192 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/approvals:
+ /tokens/{tokenId}:
+ parameters:
+ - name: tokenId
+ in: path
+ description: System-generated unique token identifier
+ required: true
+ schema:
+ type: string
get:
- summary: List agent transaction approval requests
- description: |
- Retrieve a paginated list of agent actions that require platform approval. Filter by `agentId` or `customerId` to scope results to a specific agent or customer. Approve or reject individual actions via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`.
- operationId: listAgentApprovals
+ summary: Get API token by ID
+ description: Retrieve an API token by their system-generated ID
+ operationId: getTokenById
tags:
- - Agent Management
+ - API Tokens
security:
- BasicAuth: []
- parameters:
- - name: agentId
- in: query
- description: Filter by agent ID
- required: false
- schema:
- type: string
- - name: customerId
- in: query
- description: Filter by customer ID
- required: false
- schema:
- type: string
- - name: startDate
- in: query
- description: Filter by start date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: endDate
- in: query
- description: Filter by end date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
- - name: sortOrder
- in: query
- description: Order to sort results in
- required: false
- schema:
- type: string
- enum:
- - asc
- - desc
- default: desc
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentActionListResponse'
- '400':
- description: Bad request - Invalid parameters
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/ApiToken'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Token not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me:
- get:
- summary: Get current agent
- description: |
- Retrieve the authenticated agent's own profile, policy, and current usage. This endpoint is called by the agent software itself using its own credentials (obtained via device code redemption) rather than platform credentials.
- operationId: getAgentMe
+ delete:
+ summary: Delete API token by ID
+ description: Delete an API token by their system-generated ID
+ operationId: deleteTokenById
tags:
- - Agent Operations
+ - API Tokens
security:
- - AgentAuth: []
+ - BasicAuth: []
responses:
- '200':
- description: Successful operation
+ '204':
+ description: API token deleted successfully
+ '400':
+ description: Bad request
content:
application/json:
schema:
- $ref: '#/components/schemas/Agent'
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '404':
+ description: Token not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/{agentId}:
+ /internal-accounts/{id}:
parameters:
- - name: agentId
+ - name: id
in: path
- description: System-generated unique agent identifier
+ description: The id of the internal account to update.
required: true
schema:
type: string
- get:
- summary: Get agent by ID
- description: Retrieve an agent by its system-generated ID.
- operationId: getAgentById
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ patch:
+ summary: Update internal account
+ description: |
+ Update mutable fields on an internal account. Today this supports updating the wallet privacy setting for an Embedded Wallet internal account.
+
+ Updating wallet privacy is a two-step signed-retry flow:
+
+ 1. Call `PATCH /internal-accounts/{id}` with the request body `{ "privateEnabled": true }` and no signature headers. Grid returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the same update fields submitted in step 1. The signed retry returns `200` with the updated internal account.
+ operationId: updateInternalAccount
tags:
- - Agent Management
+ - Internal Accounts
security:
- BasicAuth: []
+ parameters:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/InternalAccountUpdateRequest'
+ examples:
+ updateWalletPrivacy:
+ summary: Update wallet privacy request (both steps)
+ value:
+ privateEnabled: true
responses:
'200':
- description: Successful operation
+ description: Signed retry accepted. Returns the updated internal account.
content:
application/json:
schema:
- $ref: '#/components/schemas/Agent'
+ $ref: '#/components/schemas/InternalAccount'
+ examples:
+ enabled:
+ summary: Wallet privacy enabled
+ value:
+ id: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ type: EMBEDDED_WALLET
+ status: ACTIVE
+ balance:
+ amount: 12550
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ totalBalance:
+ amount: 12550
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ fundingPaymentInstructions: []
+ privateEnabled: true
+ createdAt: '2026-04-08T15:30:00Z'
+ updatedAt: '2026-04-08T15:35:02Z'
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` (which binds the submitted update fields) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SignedRequestChallenge'
+ examples:
+ challenge:
+ summary: Internal account update challenge
+ value:
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"encoding":"PAYLOAD_ENCODING_HEXADECIMAL","hashFunction":"HASH_FUNCTION_NO_OP","payload":"9f3b...","signWith":"sp1q..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2"}'
+ requestId: Request:019542f5-b3e7-1d02-0000-000000000010
+ expiresAt: '2026-04-08T15:35:00Z'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending internal account update challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry body does not match the update fields bound into `payloadToSign` on the initial call.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent not found
+ description: Internal account not found
content:
application/json:
schema:
@@ -5798,27 +5312,69 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- patch:
- summary: Update agent
- description: Update an agent's name or paused state.
- operationId: updateAgent
+ /internal-accounts/{id}/export:
+ post:
+ summary: Export internal account wallet credentials
+ description: |
+ Export the wallet credentials of an Embedded Wallet internal account. The returned wallet credentials are HPKE-encrypted to the `clientPublicKey` supplied in the request body.
+
+ Export is a two-step signed-retry flow (same pattern as add-additional credential, revoke credential, and revoke session):
+
+ 1. Call `POST /internal-accounts/{id}/export` with the request body `{ "clientPublicKey": "..." }` and no signature headers. Grid binds the `clientPublicKey` into the `payloadToSign` it returns, so the subsequent stamp in `Grid-Wallet-Signature` commits to the target encryption key. The response is `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key.
+
+ The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against.
+ operationId: exportInternalAccount
tags:
- - Agent Management
+ - Internal Accounts
security:
- BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the internal account to export.
+ required: true
+ schema:
+ type: string
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified authentication credential on the target internal account. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentUpdateRequest'
+ $ref: '#/components/schemas/InternalAccountExportRequest'
+ examples:
+ export:
+ summary: Export request (both steps)
+ value:
+ clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
responses:
'200':
- description: Agent updated successfully
+ description: Signed retry accepted. Returns the encrypted wallet credentials.
content:
application/json:
schema:
- $ref: '#/components/schemas/Agent'
+ $ref: '#/components/schemas/InternalAccountExportResponse'
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SignedRequestChallenge'
'400':
description: Bad request
content:
@@ -5826,13 +5382,13 @@ paths:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending export challenge for this internal account, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent not found
+ description: Internal account not found
content:
application/json:
schema:
@@ -5843,79 +5399,167 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- delete:
- summary: Delete agent
- description: Permanently delete an agent. Connected agent software will lose access immediately.
- operationId: deleteAgent
- tags:
- - Agent Management
- security:
- - BasicAuth: []
- responses:
- '204':
- description: Agent deleted successfully
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Agent not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error500'
- /agents/{agentId}/policy:
- parameters:
- - name: agentId
- in: path
- description: System-generated unique agent identifier
- required: true
- schema:
- type: string
- patch:
- summary: Update agent policy
+ /auth/credentials:
+ post:
+ summary: Create an authentication credential
description: |
- Partially update an agent's policy. Only provided fields will be updated; omitted fields retain their current values. Policy changes take effect immediately.
- operationId: updateAgentPolicy
+ Register an authentication credential for an Embedded Wallet customer.
+
+ Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered.
+
+ Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`.
+ operationId: createAuthCredential
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the target internal account. Required on the signed retry.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry when registering a credential; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentPolicyUpdateRequest'
+ $ref: '#/components/schemas/AuthCredentialCreateRequestOneOf'
+ examples:
+ emailOtp:
+ summary: Add an email OTP credential
+ value:
+ type: EMAIL_OTP
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ smsOtp:
+ summary: Add an SMS OTP credential
+ value:
+ type: SMS_OTP
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ oauth:
+ summary: Add an OAuth credential
+ value:
+ type: OAUTH
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
+ passkey:
+ summary: Add a passkey credential
+ value:
+ type: PASSKEY
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ nickname: iPhone Face-ID
+ challenge: ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx
+ attestation:
+ credentialId: AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY
+ clientDataJson: eyJjaGFsbGVuZ2UiOiJBcktRaTJ5QVlIUGxnbkpORkJsbmVJd2NoUWRXWEJPVHJkQi1BbU1VQjIxTHgiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0
+ attestationObject: o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow
+ transports:
+ - internal
+ - hybrid
responses:
- '200':
- description: Agent policy updated successfully
+ '201':
+ description: Authentication credential created successfully. The body is the created `AuthMethod`. For `EMAIL_OTP`, the nickname is the customer email tied to the internal account; for `SMS_OTP`, it is the customer phone number. OTP responses that trigger a secure OTP challenge carry `otpEncryptionTargetBundle` — the HPKE target bundle the client uses to encrypt the OTP attempt on the subsequent `POST /auth/credentials/{id}/verify`. First-time EMAIL_OTP wallet bootstrap responses may omit that bundle; if it is absent, call `POST /auth/credentials/{id}/challenge` for the new credential to issue a fresh OTP and receive `otpEncryptionTargetBundle` before verifying. For `PASSKEY`, the credential must be authenticated for the first time via `POST /auth/credentials/{id}/challenge` followed by `POST /auth/credentials/{id}/verify` to produce a session — there is no inline authentication challenge on the registration response.
content:
application/json:
schema:
- $ref: '#/components/schemas/Agent'
+ $ref: '#/components/schemas/AuthMethodResponse'
+ examples:
+ emailOtp:
+ summary: Email OTP credential created
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: EMAIL_OTP
+ nickname: example@lightspark.com
+ otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ smsOtp:
+ summary: SMS OTP credential created
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: SMS_OTP
+ nickname: '+14155550123'
+ otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ oauth:
+ summary: OAuth credential created
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: OAUTH
+ nickname: example@lightspark.com
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ passkey:
+ summary: Passkey credential created
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: PASSKEY
+ nickname: iPhone Face-ID
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ '202':
+ description: Challenge issued. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account, then send that full stamp as `Grid-Wallet-Signature` and echo `requestId` as `Request-Id` on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthSignedRequestChallenge'
+ examples:
+ emailOtp:
+ summary: Additional email OTP credential challenge
+ value:
+ type: EMAIL_OTP
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userEmail":"jane@example.com","userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_EMAIL"}'
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
+ smsOtp:
+ summary: Additional SMS OTP credential challenge
+ value:
+ type: SMS_OTP
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F...","userPhoneNumber":"+14155550123"},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER"}'
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
+ oauth:
+ summary: Additional OAuth credential challenge
+ value:
+ type: OAUTH
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"oauthProviders":[{"oidcToken":"eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9...","providerName":"Google"}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS"}'
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
+ passkey:
+ summary: Additional passkey credential challenge
+ value:
+ type: PASSKEY
+ payloadToSign: '{"organizationId":"org_2m9F...","parameters":{"authenticators":[{"attestation":{"attestationObject":"o2NmbXRk...","clientDataJson":"eyJjaGFsbGVuZ2UiOiJBcktRa...","credentialId":"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY"},"authenticatorName":"iPhone Face-ID","challenge":"ArkQi2yAYHPlgnJNFBlneIwchQdWXBOTrdB-AmMUB21Lx","transports":["internal","hybrid"]}],"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2"}'
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
'400':
- description: Bad request
+ description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `SMS_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an SMS OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer.
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent not found
+ description: Internal account not found
content:
application/json:
schema:
@@ -5926,32 +5570,62 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/{agentId}/device-codes:
- parameters:
- - name: agentId
- in: path
- description: System-generated unique agent identifier
- required: true
- schema:
- type: string
- post:
- summary: Regenerate a device code
- description: |
- Generate a new device code for an existing agent. Use this when the original device code has expired before being redeemed, or when the agent software needs to be reinstalled. Any previously issued unredeemed device codes for this agent are invalidated.
- operationId: regenerateAgentDeviceCode
+ get:
+ summary: List authentication credentials
+ description: |-
+ Retrieve all authentication credentials registered on an Embedded Wallet internal account.
+
+ The response is not paginated: an internal account is expected to have a small, bounded number of credentials (typically 1–5), so all results are returned inline. Additional per-credential detail (such as active session expiry) is available on `GET /auth/sessions`.
+ operationId: listAuthCredentials
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: accountId
+ in: query
+ description: Internal account id whose authentication credentials to list.
+ required: true
+ schema:
+ type: string
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
responses:
- '201':
- description: New device code generated successfully
+ '200':
+ description: Authentication credentials registered on the internal account. Returns an empty `data` array when the internal account has no credentials or when `accountId` does not match any internal account visible to the caller.
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentDeviceCode'
+ $ref: '#/components/schemas/AuthCredentialListResponse'
+ examples:
+ multipleCredentials:
+ summary: Internal account with multiple authentication credentials
+ value:
+ data:
+ - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: EMAIL_OTP
+ nickname: example@lightspark.com
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:01Z'
+ - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000004
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: OAUTH
+ nickname: example@lightspark.com
+ createdAt: '2026-04-08T15:35:00Z'
+ updatedAt: '2026-04-08T15:35:00Z'
+ - id: AuthMethod:019542f5-b3e7-1d02-0000-000000000003
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: PASSKEY
+ credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
+ nickname: iPhone Face-ID
+ createdAt: '2026-04-09T10:15:00Z'
+ updatedAt: '2026-04-09T10:15:00Z'
+ empty:
+ summary: No credentials registered on the account
+ value:
+ data: []
'400':
- description: Bad request
+ description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier.
content:
application/json:
schema:
@@ -5962,332 +5636,352 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '404':
- description: Agent not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Agent already has an active connection and cannot regenerate a device code
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/{agentId}/actions/{actionId}/approve:
- parameters:
- - name: agentId
- in: path
- description: System-generated unique agent identifier
- required: true
- schema:
- type: string
- - name: actionId
- in: path
- description: Unique identifier of the agent action to approve
- required: true
- schema:
- type: string
- post:
- summary: Approve an agent action
+ /auth/credentials/{id}:
+ delete:
+ summary: Revoke an authentication credential
description: |
- Approve a pending agent action, allowing Grid to proceed with execution. The action must have status `PENDING_APPROVAL`. Once approved, Grid executes the underlying operation (quote execution or transfer) and the action transitions to `APPROVED`.
- For `EXECUTE_QUOTE` actions, note that the underlying quote may have expired between submission and approval — in that case the action will transition to `FAILED` instead.
- This endpoint is called by the platform's backend using platform credentials, not by the agent itself.
- operationId: approveAgentAction
+ Revoke an authentication credential on an Embedded Wallet internal account.
+
+ Revocation is a two-step flow because it must be authorized by a session on a *different* credential on the same internal account:
+
+ 1. Call `DELETE /auth/credentials/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of an existing verified credential on the same internal account — other than the one being revoked — to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`.
+
+ The account must retain at least one authentication credential; an account with only a single credential cannot use this endpoint to revoke it.
+ operationId: revokeAuthCredential
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the authentication credential to revoke (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
+ required: true
+ schema:
+ type: string
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of an existing verified authentication credential on the same internal account (other than the one being revoked). Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
responses:
- '200':
- description: Action approved successfully. Returns the updated AgentAction.
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of an existing verified credential on the same internal account (other than the one being revoked), then echo `requestId` on the retry.
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/AuthSignedRequestChallenge'
+ '204':
+ description: Authentication credential revoked successfully.
'400':
- description: Bad request - Action cannot be approved
+ description: Bad request. Also returned when the target internal account has only a single authentication credential, which cannot be revoked via this endpoint.
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this credential, or when the `Request-Id` does not match an unexpired pending challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent or action not found
+ description: Authentication credential not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Action is not pending approval or has already been processed
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/{agentId}/actions/{actionId}/reject:
- parameters:
- - name: agentId
- in: path
- description: System-generated unique agent identifier
- required: true
- schema:
- type: string
- - name: actionId
- in: path
- description: Unique identifier of the agent action to reject
- required: true
- schema:
- type: string
+ /auth/credentials/{id}/verify:
post:
- summary: Reject an agent action
+ summary: Verify an authentication credential
description: |
- Reject a pending agent action, preventing execution. The action must have status `PENDING_APPROVAL`. Once rejected, the action transitions to `REJECTED` and the underlying operation is not executed.
- This endpoint is called by the platform's backend using platform credentials, not by the agent itself.
- operationId: rejectAgentAction
+ Complete the verification step for a previously created authentication credential and issue a session.
+
+ For `EMAIL_OTP` and `SMS_OTP` credentials, submit the `encryptedOtpBundle` produced by HPKE-encrypting `{otp_code, public_key}` under the `otpEncryptionTargetBundle` returned from registration when present, or from `POST /auth/credentials/{id}/challenge` when registration omitted it or the OTP must be reissued. The server is a pass-through and never sees the plaintext OTP code. On success the response is `202` with a `payloadToSign` carrying the `verificationToken` bound to the client's TEK public key — sign that token with the matching TEK private key, then retry the same request with the full stamp in `Grid-Wallet-Signature` and the `requestId` echoed in `Request-Id`. The signed retry returns `200` with the issued `AuthSession`. The TEK public key becomes the session API key on successful completion.
+ In sandbox mode, the OTP flow runs real HPKE end-to-end against a sandbox enclave keypair — clients build a real `encryptedOtpBundle` against the sandbox `otpEncryptionTargetBundle` and sign a real `verificationToken` with their TEK keypair. The only sandbox shortcut is the magic OTP code (`"000000"`) the user "receives" instead of a real email or SMS delivery.
+
+ For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request.
+
+ On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires.
+ operationId: verifyAuthCredential
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the authentication credential to verify (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
+ required: true
+ schema:
+ type: string
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the TEK (Target Encryption Key) keypair the client generated for this login. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification. Not used by `OAUTH` or `PASSKEY` verification, which complete in a single call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response from this endpoint, echoed back exactly here so the server can correlate the signed retry with the issued challenge. Required on the signed retry that completes an `EMAIL_OTP` or `SMS_OTP` verification; must be paired with `Grid-Wallet-Signature`. For `PASSKEY` verification, the `requestId` issued from `POST /auth/credentials/{id}/challenge` is echoed here instead so the server can correlate the assertion with the pending challenge.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
- required: false
+ required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentActionRejectRequest'
+ $ref: '#/components/schemas/AuthCredentialVerifyRequestOneOf'
+ examples:
+ emailOtp:
+ summary: Verify an email OTP credential (first leg)
+ value:
+ type: EMAIL_OTP
+ encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
+ smsOtp:
+ summary: Verify an SMS OTP credential (first leg)
+ value:
+ type: SMS_OTP
+ encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
+ emailOtpSignedRetry:
+ summary: Signed retry completing an email OTP verification
+ description: Same request body as the first leg, plus the `Grid-Wallet-Signature` and `Request-Id` headers carrying the stamp over the `verificationToken` and the `requestId` from the prior `202` response.
+ value:
+ type: EMAIL_OTP
+ encryptedOtpBundle: '{"encappedPublic":"044f631a2d890bc6668d997ee184e190650d06adf970987568ec641214a00403b73effe1ef406c60a5cde8508a4484567ddb8056fbd493bee614cd727aef02a838","ciphertext":"1fa1023390a56539aa48cbb380aa28f544ed5cc04861566bb806e25ba026f14660eaf4140a05b388dd012eaa899759a6a92576cdca8c1b7d12e147bd96cc26ed9f74886794155d8ac5cf0fdc"}'
+ oauth:
+ summary: Verify an OAuth credential
+ value:
+ type: OAUTH
+ oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q
+ clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ passkey:
+ summary: Verify a passkey credential
+ value:
+ type: PASSKEY
+ assertion:
+ credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
+ clientDataJson: eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJjbGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5kb250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0
+ authenticatorData: PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA
+ signature: MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6jhd45bDx92wjXKs900
responses:
'200':
- description: Action rejected successfully. Returns the updated AgentAction.
+ description: Authentication credential verified and session issued
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/AuthSession'
+ '202':
+ description: Verification challenge issued. Returned only for OTP credentials, on the first leg of the secure OTP login flow. Build an API-key stamp over `payloadToSign` (the `verificationToken`) with the TEK keypair the client generated for this login, then resubmit the same request with that full stamp as `Grid-Wallet-Signature` and `requestId` echoed as `Request-Id` to receive the issued session on the signed retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthSignedRequestChallenge'
+ examples:
+ emailOtp:
+ summary: Email OTP verification challenge (sign and retry)
+ value:
+ type: EMAIL_OTP
+ payloadToSign: eyJhbGciOiJFUzI1NiIsImtpZCI6InR1cm5rZXkifQ.eyJzdWIiOiJUWnk2NkVPa1RGYTd2NkpXZ0VxaVgyZGFXOENXc2pMQzVDVU9aRUlGY3hzIiwiaWF0IjoxNzc5NDA3MjIxLCJleHAiOjE3Nzk0MTA4MjF9.gKX9MWYGkw8Y55bgzsgrRftvUHFruIe8yu0w9Kpjp5qnrZnXcTV71WVoltGPsr015IY_oRTOkIFLHmiGNG9zBw
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
'400':
- description: Bad request - Action cannot be rejected
+ description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP` or `SMS_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. For OTP signed retries, returned when `Grid-Wallet-Signature` is missing, malformed, signed by a public key that does not match the one bound into the `verificationToken`, or when `Request-Id` does not match an unexpired pending verification challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Agent or action not found
+ description: Authentication credential not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '409':
- description: Conflict - Action is not pending approval or has already been processed
+ '429':
+ description: Too many requests. Returned with `RATE_LIMITED` when verification attempts for this credential happen too frequently (for example, repeated bad OTPs or rapid-fire reauthentication retries). Clients should back off and retry after the interval indicated by the `Retry-After` response header.
+ headers:
+ Retry-After:
+ description: Number of seconds to wait before retrying the request.
+ schema:
+ type: integer
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/device-codes/{code}/status:
- parameters:
- - name: code
- in: path
- description: The device code to check
- required: true
- schema:
- type: string
- get:
- summary: Get device code status
+ /auth/credentials/{id}/challenge:
+ post:
+ summary: Re-issue an authentication credential challenge
description: |
- Check whether a device code has been redeemed. Use this to poll for agent installation completion after creating an agent.
- operationId: getAgentDeviceCodeStatus
+ Re-issue the challenge for an existing authentication credential.
+
+ For `EMAIL_OTP` and `SMS_OTP` credentials, this triggers a new one-time password to the contact on file and returns a fresh `otpEncryptionTargetBundle` for the client to HPKE-encrypt the OTP attempt against. After the user receives the new OTP, build the `encryptedOtpBundle` under the new target bundle and call `POST /auth/credentials/{id}/verify` to begin the secure OTP login flow.
+
+ `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`.
+
+ For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session.
+ operationId: challengeAuthCredential
tags:
- - Agent Management
+ - Embedded Wallet Auth
security:
- BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the authentication credential to re-challenge (the `id` field of the `AuthMethod` returned from `POST /auth/credentials`).
+ required: true
+ schema:
+ type: string
+ requestBody:
+ description: Request body. Required when re-challenging a `PASSKEY` credential (must carry `clientPublicKey`). Ignored for `EMAIL_OTP` and `SMS_OTP`, where the credential type alone is sufficient — the OTP is delivered out-of-band. OAuth credentials do not use this endpoint.
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthCredentialChallengeRequest'
+ examples:
+ passkey:
+ summary: Re-challenge a passkey credential
+ value:
+ clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
+ emailOtp:
+ summary: Re-challenge an email-OTP credential (empty body)
+ value: {}
+ smsOtp:
+ summary: Re-challenge an SMS-OTP credential (empty body)
+ value: {}
responses:
'200':
- description: Successful operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AgentDeviceCodeStatusResponse'
- '401':
- description: Unauthorized
+ description: Challenge re-issued for the authentication credential. For `EMAIL_OTP` and `SMS_OTP` the body is a plain `AuthMethod` and a new OTP has been sent. For `PASSKEY` the body is a `PasskeyAuthChallenge` carrying the passkey `credentialId`, freshly issued `challenge`, `requestId`, and `expiresAt` required to complete reauthentication via `POST /auth/credentials/{id}/verify`.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
- '404':
- description: Device code not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
+ $ref: '#/components/schemas/AuthCredentialResponseOneOf'
+ examples:
+ emailOtp:
+ summary: Email OTP challenge re-issued
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: EMAIL_OTP
+ nickname: example@lightspark.com
+ otpEncryptionTargetBundle: '''{version:v1.0.0,data:7b227461726765745075626c6963...,dataSignature:30450221...,enclaveQuorumPublic:04a1b2c3...}'''
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:35:00Z'
+ passkey:
+ summary: Passkey reauthentication challenge issued
+ value:
+ id: AuthMethod:019542f5-b3e7-1d02-0000-000000000001
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: PASSKEY
+ credentialId: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew
+ nickname: iPhone Face-ID
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:35:00Z'
+ challenge: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729
+ requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt: '2026-04-08T15:35:00Z'
+ '400':
+ description: Bad request
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- /agents/device-codes/{code}/redeem:
- parameters:
- - name: code
- in: path
- description: The device code to redeem
- required: true
- schema:
- type: string
- post:
- summary: Redeem device code
- description: |
- Redeem a device code to obtain agent credentials. This endpoint is called by the agent software during installation. On success, returns a Bearer access token that the agent uses for all subsequent API calls. The token is returned only once and must be stored securely.
- This endpoint does not require platform authentication — the device code itself serves as proof of authorization.
- operationId: redeemAgentDeviceCode
- tags:
- - Agent Management
- security: []
- responses:
- '200':
- description: Device code redeemed successfully
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse'
- '400':
- description: Bad request (e.g., code already redeemed or expired)
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Authentication credential not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
- '404':
- description: Device code not found
+ $ref: '#/components/schemas/Error404'
+ '429':
+ description: Too many requests. Returned with `RATE_LIMITED` when challenge re-issues are requested more frequently than the credential challenge rate limit allows. Clients should back off and retry after the interval indicated by the `Retry-After` response header.
+ headers:
+ Retry-After:
+ description: Number of seconds to wait before retrying the request.
+ schema:
+ type: integer
content:
application/json:
schema:
- $ref: '#/components/schemas/Error404'
+ $ref: '#/components/schemas/Error429'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/transactions:
+ /auth/sessions:
get:
- summary: List agent transactions
- description: |
- Retrieve a paginated list of transactions for the authenticated agent's customer. Results are automatically scoped to the agent's associated customer — no customer filter is needed or accepted.
- operationId: agentListTransactions
+ summary: List active sessions
+ description: |-
+ Retrieve all active authentication sessions on an Embedded Wallet internal account. A session is created each time a credential is verified via `POST /auth/credentials/{id}/verify`, and remains active until its `expiresAt` passes or it is revoked via `DELETE /auth/sessions/{id}`.
+
+ The response is not paginated: an internal account is expected to have a small, bounded number of concurrent sessions (one per signed-in device, typically 1–4), so all results are returned inline.
+ operationId: listAuthSessions
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: accountIdentifier
- in: query
- description: Filter by account identifier (matches either sender or receiver)
- required: false
- schema:
- type: string
- - name: senderAccountIdentifier
- in: query
- description: Filter by sender account identifier
- required: false
- schema:
- type: string
- - name: receiverAccountIdentifier
- in: query
- description: Filter by receiver account identifier
- required: false
- schema:
- type: string
- - name: status
- in: query
- description: Filter by transaction status
- required: false
- schema:
- $ref: '#/components/schemas/TransactionStatus'
- - name: type
- in: query
- description: Filter by transaction type
- required: false
- schema:
- $ref: '#/components/schemas/TransactionType'
- - name: reference
- in: query
- description: Filter by reference
- required: false
- schema:
- type: string
- - name: startDate
- in: query
- description: Filter by start date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: endDate
- in: query
- description: Filter by end date (inclusive) in ISO 8601 format
- required: false
- schema:
- type: string
- format: date-time
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
- - name: sortOrder
+ - name: accountId
in: query
- description: Order to sort results in
- required: false
+ description: Internal account id whose sessions to list.
+ required: true
schema:
type: string
- enum:
- - asc
- - desc
- default: desc
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
responses:
'200':
- description: Successful operation
+ description: Active authentication sessions on the internal account. Returns an empty `data` array when the internal account has no active sessions or when `accountId` does not match any internal account visible to the caller.
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionListResponse'
+ $ref: '#/components/schemas/SessionListResponse'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request. Returned with `INVALID_INPUT` when the `accountId` query parameter is missing or not a valid `InternalAccount:` identifier.
content:
application/json:
schema:
@@ -6304,38 +5998,66 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/transactions/{transactionId}:
- parameters:
- - name: transactionId
- in: path
- description: Unique identifier of the transaction
- required: true
- schema:
- type: string
- get:
- summary: Get agent transaction by ID
+ /auth/sessions/{id}:
+ delete:
+ summary: Revoke an authentication session
description: |
- Retrieve a specific transaction belonging to the authenticated agent's customer. Returns 404 if the transaction exists but belongs to a different customer.
- operationId: agentGetTransaction
+ Revoke an authentication session on an Embedded Wallet internal account. Revocation is a two-step signed-retry flow:
+
+ 1. Call `DELETE /auth/sessions/{id}` with no headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of a verified session on the same internal account (this can be the session being revoked, for self-logout) to build an API-key stamp over `payloadToSign`, then retry the same `DELETE` request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `204`.
+
+ Sessions also expire on their own. `404` is returned whenever the `id` does not match an active session — whether the session was never issued, was already revoked by a prior call, or has expired past its `expiresAt`. The response code reflects the resource state, not an error in the client's flow: re-revoking an already-revoked or expired session is safe and idempotent at the user intent level.
+ operationId: revokeAuthSession
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the session to revoke.
+ required: true
+ schema:
+ type: string
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified session on the same internal account. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
responses:
- '200':
- description: Successful operation
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair of a verified session on the same internal account, then echo `requestId` on the retry.
content:
application/json:
schema:
- $ref: '#/components/schemas/TransactionOneOf'
+ $ref: '#/components/schemas/AuthSignedRequestChallenge'
+ '204':
+ description: Session revoked successfully.
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending revocation challenge for this session, or when the `Request-Id` does not match an unexpired pending challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Transaction not found
+ description: Session not found
content:
application/json:
schema:
@@ -6346,107 +6068,101 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/quotes:
+ /auth/sessions/{id}/refresh:
post:
- summary: Create a transfer quote
+ summary: Refresh an authentication session
description: |
- Generate a quote for a cross-currency transfer on behalf of the authenticated agent's customer. Accounts referenced in the request must belong to the agent's customer. Requires the CREATE_QUOTES permission in the agent's policy.
- If the agent's defaultExecutionMode is APPROVAL_REQUIRED, or the quote amount exceeds the agent's approvalThresholds, the resulting transaction will require explicit approval before funds move.
- operationId: agentCreateQuote
+ Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow:
+
+ 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key.
+
+ The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead.
+ operationId: refreshAuthSession
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: Idempotency-Key
+ - name: id
+ in: path
+ description: The id of the active session to refresh.
+ required: true
+ schema:
+ type: string
+ example: Session:019542f5-b3e7-1d02-0000-000000000003
+ - name: Grid-Wallet-Signature
in: header
required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ description: Full API-key stamp built over the prior `payloadToSign` with the current session API keypair. Required on the signed retry; ignored on the initial call.
schema:
type: string
- example:
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in the prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: Request:019542f5-b3e7-1d02-0000-000000000010
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/QuoteRequest'
+ $ref: '#/components/schemas/AuthSessionRefreshRequest'
+ examples:
+ refresh:
+ summary: Refresh an active session
+ value:
+ clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
responses:
'201':
- description: Transfer quote created successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Quote'
- '400':
- description: Bad request - Missing or invalid parameters
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Agent policy does not permit this operation
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
- '412':
- description: Counterparty doesn't support UMA version
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error412'
- '424':
- description: Counterparty issue
+ description: New authentication session created successfully.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error424'
- '500':
- description: Internal service error
+ $ref: '#/components/schemas/AuthSession'
+ examples:
+ session:
+ summary: Refreshed authentication session
+ value:
+ id: Session:019542f5-b3e7-1d02-0000-000000000011
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ type: EMAIL_OTP
+ encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf
+ nickname: example@lightspark.com
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:35:00Z'
+ expiresAt: '2026-04-08T15:50:00Z'
+ '202':
+ description: Challenge issued. The response contains `payloadToSign` plus a `requestId`. Build an API-key stamp over `payloadToSign` with the current session API keypair, then echo `requestId` on the signed retry.
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- /agents/me/quotes/{quoteId}:
- parameters:
- - name: quoteId
- in: path
- description: ID of the quote to retrieve
- required: true
- schema:
- type: string
- get:
- summary: Get agent quote by ID
- description: |
- Retrieve a quote created by the authenticated agent. Returns 404 if the quote exists but was not created by this agent.
- operationId: agentGetQuote
- tags:
- - Agent Operations
- security:
- - AgentAuth: []
- responses:
- '200':
- description: Quote retrieved successfully
+ $ref: '#/components/schemas/SignedRequestChallenge'
+ examples:
+ challenge:
+ summary: Session refresh challenge
+ value:
+ payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}'
+ requestId: Request:019542f5-b3e7-1d02-0000-000000000010
+ expiresAt: '2026-04-08T15:35:00Z'
+ '400':
+ description: Bad request
content:
application/json:
schema:
- $ref: '#/components/schemas/Quote'
+ $ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Returned when the `BasicAuth` credentials are missing or invalid, when the target session is no longer active and cannot be used for refresh, when the signed retry omits `Grid-Wallet-Signature`, when the provided signature is malformed or does not match the pending refresh challenge, when the `Request-Id` does not match an unexpired pending challenge, or when the retry's `clientPublicKey` does not match the one bound into `payloadToSign` on the initial call.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Quote not found
+ description: Session not found
content:
application/json:
schema:
@@ -6457,75 +6173,100 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/quotes/{quoteId}/execute:
- parameters:
- - name: quoteId
- in: path
- required: true
- description: The unique identifier of the quote to execute
- schema:
- type: string
- example: Quote:019542f5-b3e7-1d02-0000-000000000001
+ /auth/delegated-keys:
post:
- summary: Execute a quote
+ summary: Create a delegated signing key
description: |
- Execute a quote created by the authenticated agent. Requires the EXECUTE_QUOTES permission in the agent's policy.
- If the agent's policy requires approval for this amount (based on execution mode or approval thresholds), the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
- Once executed, the quote cannot be cancelled.
- operationId: agentExecuteQuote
+ Delegate Spark token-transaction signing authority for a card funding source backed by an Embedded Wallet internal account to a Grid-custodied P-256 API key. Grid uses the requested card and internal account to identify the wallet funding source, generates the keypair server-side, creates an isolated signer identity holding the public key, then policies granting that identity signing and self-revocation authority. The private key is custodied by Grid and never returned. Both activities must be authorized by the wallet owner, so creation is a three-leg signed-retry flow:
+
+ 1. Call `POST /auth/delegated-keys` with no signature headers. Grid generates the delegated keypair and the response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Use the session API keypair of a verified credential on the requested Embedded Wallet internal account to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The response is a second `202` with a new `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 3. Stamp the new `payloadToSign` with the same session keypair and retry once more with the new `Request-Id`. The signed retry returns `201` with the created `DelegatedKey` in `ACTIVE` status.
+
+ The same request body must be sent on all three legs. A flow abandoned after the second leg leaves the key in `PENDING` status: the signer identity exists but holds no policies, so it cannot sign or revoke itself. Abandoned `PENDING` keys do not block creating another delegated key. After activation, Grid uses the custodied key to authorize signing for the card's Embedded Wallet funding account in place of a session keypair; the platform never handles the key material.
+
+ Each card funding source may have at most one `ACTIVE` delegated key for its Embedded Wallet funding account; revoke the existing active key before creating a new one. A delegated key authorizes raw-payload signing for the wallet and cannot be scoped to amounts or recipients by the public API. Revoke it with `DELETE /auth/delegated-keys/{id}` when no longer needed.
+ operationId: createDelegatedKey
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: Idempotency-Key
+ - name: Grid-Wallet-Signature
in: header
required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ description: Full API-key stamp built over the prior `payloadToSign` with the session API keypair of a verified credential on the same internal account. Required on the signed retries; ignored on the initial call.
schema:
type: string
- example:
- - name: Grid-Wallet-Signature
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ - name: Request-Id
in: header
required: false
- description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ description: The `requestId` returned in the prior `202` response, echoed back exactly on the signed retry so the server can correlate it with the issued challenge. Required on the signed retries; must be paired with `Grid-Wallet-Signature`.
schema:
type: string
- example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DelegatedKeyCreateRequest'
+ examples:
+ create:
+ summary: Delegate signing to a Grid-custodied key
+ value:
+ cardId: Card:019542f5-b3e7-1d02-0000-000000000010
+ internalAccountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ nickname: Card payments key
responses:
- '200':
- description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.'
+ '201':
+ description: Delegated key created and policy granted. The key is `ACTIVE` and Grid may use it to stamp card-payment quote executions for this card funding source's Embedded Wallet funding account.
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/DelegatedKey'
+ examples:
+ delegatedKey:
+ summary: Active delegated key
+ value:
+ id: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
+ cardId: Card:019542f5-b3e7-1d02-0000-000000000010
+ fundingSourceId: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011
+ accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ publicKey: 02a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90
+ nickname: Card payments key
+ status: ACTIVE
+ createdAt: '2026-04-08T15:30:01Z'
+ updatedAt: '2026-04-08T15:30:42Z'
+ '202':
+ description: Challenge issued for the next leg. Stamp `payloadToSign` and retry the same request with `Grid-Wallet-Signature` and `Request-Id`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DelegatedKeySignedRequestChallenge'
'400':
- description: Bad request - Quote cannot be executed
+ description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`.
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing on a retry, malformed, or does not match the pending challenge, or when the `Request-Id` does not match an unexpired pending challenge.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Quote not found
+ description: Card, card funding source, or Embedded Wallet funding account not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
'409':
- description: Conflict - Quote already executed, expired, or in invalid state
+ description: An `ACTIVE` delegated key already exists for this card funding source. Revoke it with `DELETE /auth/delegated-keys/{id}` before creating a new one.
content:
application/json:
schema:
@@ -6536,81 +6277,78 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/actions:
get:
- summary: List agent's own actions
- description: |
- Retrieve a paginated list of actions submitted by the authenticated agent. Use this to poll for approval decisions after submitting an action that requires approval.
- operationId: agentListActions
+ summary: List delegated signing keys
+ description: List delegated signing keys for an Embedded Wallet internal account, a card funding source, or both, including `PENDING` keys (user created but policy leg never completed) and `REVOKED` keys. At least one of `accountId` or `fundingSourceId` must be supplied.
+ operationId: listDelegatedKeys
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: status
- in: query
- description: Filter by action status
- required: false
- schema:
- $ref: '#/components/schemas/AgentActionStatus'
- - name: limit
+ - name: accountId
in: query
- description: Maximum number of results to return (default 20, max 100)
required: false
+ description: The id of the internal account whose delegated keys to list.
schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
+ type: string
+ example: InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - name: fundingSourceId
in: query
- description: Cursor for pagination (returned from previous request)
required: false
+ description: The id of the card funding source whose delegated keys to list.
schema:
type: string
+ example: CardFundingSource:019542f5-b3e7-1d02-0000-000000000011
responses:
'200':
- description: Successful operation
+ description: Delegated keys matching the supplied filters. Returns an empty `data` array when no matching delegated keys are visible to the caller.
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentActionListResponse'
- '401':
- description: Unauthorized
+ $ref: '#/components/schemas/DelegatedKeyListResponse'
+ '400':
+ description: Bad request
content:
application/json:
schema:
- $ref: '#/components/schemas/Error401'
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/actions/{actionId}:
- parameters:
- - name: actionId
- in: path
- description: Unique identifier of the agent action
- required: true
- schema:
- type: string
+ /auth/delegated-keys/{id}:
get:
- summary: Get an agent action
- description: |
- Retrieve a specific action submitted by the authenticated agent. Poll this endpoint after submitting an action that requires approval to check whether it has been approved, rejected, or has failed.
- operationId: agentGetAction
+ summary: Get a delegated signing key
+ description: Retrieve a delegated signing key by its system-generated id.
+ operationId: getDelegatedKeyById
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ description: The id of the delegated key to retrieve (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys` or `GET /auth/delegated-keys`).
+ required: true
+ schema:
+ type: string
+ example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/DelegatedKey'
'401':
description: Unauthorized
content:
@@ -6618,7 +6356,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Action not found
+ description: Delegated key not found
content:
application/json:
schema:
@@ -6629,69 +6367,42 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/transfer-in:
- post:
- summary: Create a transfer-in
+ delete:
+ summary: Revoke a delegated signing key
description: |
- Transfer funds from an external account to an internal account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy.
- If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
- This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the payment instructions on the internal account to deposit funds.
- operationId: agentCreateTransferIn
+ Revoke an `ACTIVE` delegated signing key. Grid uses the custodied delegated key to authorize deleting its own signer identity. Deleting the identity also removes its API key, after which the delegated key can no longer sign. The response is `204` when revocation completes.
+
+ The underlying signing policies are left in place. Their consensus references the now-deleted signer identity, so they can never authorize anything, and deleting them is unnecessary for correctness or security.
+ operationId: revokeDelegatedKey
tags:
- - Agent Operations
+ - Embedded Wallet Auth
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ - name: id
+ in: path
+ description: The id of the delegated key to revoke (the `id` field of the `DelegatedKey` returned from `POST /auth/delegated-keys`).
+ required: true
schema:
type: string
- example: 550e8400-e29b-41d4-a716-446655440000
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TransferInRequest'
- examples:
- transferIn:
- summary: Transfer from external to internal account
- value:
- source:
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- amount: 12550
+ example: DelegatedKey:019542f5-b3e7-1d02-0000-000000000021
responses:
- '201':
- description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/AgentAction'
+ '204':
+ description: Delegated key revoked. The key can no longer authorize signing.
'400':
- description: Bad request - Invalid parameters
+ description: Bad request. Returned when the delegated key has already been revoked or is not `ACTIVE`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Account not found
+ description: Delegated key not found
content:
application/json:
schema:
@@ -6702,50 +6413,31 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/transfer-out:
+ /agents:
post:
- summary: Create a transfer-out
+ summary: Create an agent
description: |
- Transfer funds from an internal account to an external account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy.
- If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
- operationId: agentCreateTransferOut
+ Create a new agent with a specified policy. Returns the created agent and a device code that must be redeemed by the agent software to complete installation.
+ operationId: createAgent
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
- parameters:
- - name: Idempotency-Key
- in: header
- required: false
- description: |
- A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
- schema:
- type: string
- example: 550e8400-e29b-41d4-a716-446655440000
+ - BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/TransferOutRequest'
- examples:
- transferOut:
- summary: Transfer from internal to external account
- value:
- source:
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- destination:
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- amount: 12550
+ $ref: '#/components/schemas/AgentCreateRequest'
responses:
'201':
- description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated.
+ description: Agent created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentAction'
+ $ref: '#/components/schemas/AgentCreateResponse'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request
content:
application/json:
schema:
@@ -6756,47 +6448,67 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
- '404':
- description: Account not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error404'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/internal-accounts:
get:
- summary: List agent's internal accounts
- description: |
- Retrieve the internal accounts belonging to the customer this agent operates on behalf of. Use this to discover available source accounts for transfers and quotes, and to verify which accounts are accessible under the agent's `accountRestrictions` policy.
- operationId: agentListInternalAccounts
+ summary: List agents
+ description: Retrieve a paginated list of agents for the authenticated platform.
+ operationId: listAgents
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: currency
+ - name: customerId
in: query
- description: Filter by currency code
+ description: Filter by customer ID
required: false
schema:
type: string
- - name: type
+ - name: isPaused
in: query
- description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for the customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for platform-managed holding accounts.
+ description: Filter by paused status
required: false
schema:
- $ref: '#/components/schemas/InternalAccountType'
+ type: boolean
+ - name: isConnected
+ in: query
+ description: Filter by connection status (whether the device code has been redeemed)
+ required: false
+ schema:
+ type: boolean
+ - name: createdAfter
+ in: query
+ description: Filter agents created after this timestamp (inclusive)
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: createdBefore
+ in: query
+ description: Filter agents created before this timestamp (inclusive)
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: updatedAfter
+ in: query
+ description: Filter agents updated after this timestamp (inclusive)
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: updatedBefore
+ in: query
+ description: Filter agents updated before this timestamp (inclusive)
+ required: false
+ schema:
+ type: string
+ format: date-time
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -6818,7 +6530,13 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/InternalAccountListResponse'
+ $ref: '#/components/schemas/AgentListResponse'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -6831,23 +6549,43 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/external-accounts:
+ /agents/approvals:
get:
- summary: List agent external accounts
+ summary: List agent transaction approval requests
description: |
- Retrieve a paginated list of external accounts belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
- operationId: agentListExternalAccounts
+ Retrieve a paginated list of agent actions that require platform approval. Filter by `agentId` or `customerId` to scope results to a specific agent or customer. Approve or reject individual actions via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`.
+ operationId: listAgentApprovals
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
+ - BasicAuth: []
parameters:
- - name: currency
+ - name: agentId
in: query
- description: Filter by currency code
+ description: Filter by agent ID
+ required: false
+ schema:
+ type: string
+ - name: customerId
+ in: query
+ description: Filter by customer ID
+ required: false
+ schema:
+ type: string
+ - name: startDate
+ in: query
+ description: Filter by start date (inclusive) in ISO 8601 format
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: endDate
+ in: query
+ description: Filter by end date (inclusive) in ISO 8601 format
required: false
schema:
type: string
+ format: date-time
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -6863,13 +6601,23 @@ paths:
required: false
schema:
type: string
- responses:
- '200':
- description: Successful operation
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
+ responses:
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccountListResponse'
+ $ref: '#/components/schemas/AgentActionListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -6888,105 +6636,58 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- post:
- summary: Add an external account
+ /agents/me:
+ get:
+ summary: Get current agent
description: |
- Register a new external bank account or wallet for the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. The `customerId` field is optional and will be inferred from the agent's associated customer if omitted.
- operationId: agentCreateExternalAccount
+ Retrieve the authenticated agent's own profile, policy, and current usage. This endpoint is called by the agent software itself using its own credentials (obtained via device code redemption) rather than platform credentials.
+ operationId: getAgentMe
tags:
- Agent Operations
security:
- AgentAuth: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ExternalAccountCreateRequest'
- examples:
- usBankAccount:
- summary: Create external US bank account
- value:
- currency: USD
- accountInfo:
- accountType: USD_ACCOUNT
- accountNumber: '12345678901'
- routingNumber: '123456789'
- bankAccountType: CHECKING
- bankName: Chase Bank
- beneficiary:
- beneficiaryType: INDIVIDUAL
- fullName: John Doe
- birthDate: '1990-01-15'
- nationality: US
- address:
- line1: 123 Main Street
- city: San Francisco
- state: CA
- postalCode: '94105'
- country: US
- sparkWallet:
- summary: Create external Spark wallet
- value:
- currency: BTC
- accountInfo:
- accountType: SPARK_WALLET
- address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
responses:
- '201':
- description: External account created successfully
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ExternalAccount'
- '400':
- description: Bad request
+ '200':
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/Agent'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - External account already exists
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error409'
'500':
description: Internal service error
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /agents/me/external-accounts/{externalAccountId}:
+ /agents/{agentId}:
parameters:
- - name: externalAccountId
+ - name: agentId
in: path
- description: System-generated unique external account identifier
+ description: System-generated unique agent identifier
required: true
schema:
type: string
get:
- summary: Get agent external account by ID
- description: |
- Retrieve an external account belonging to the authenticated agent's customer. Returns 404 if the account exists but belongs to a different customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
- operationId: agentGetExternalAccount
+ summary: Get agent by ID
+ description: Retrieve an agent by its system-generated ID.
+ operationId: getAgentById
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
+ - BasicAuth: []
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/ExternalAccount'
+ $ref: '#/components/schemas/Agent'
'401':
description: Unauthorized
content:
@@ -6994,7 +6695,52 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: External account not found
+ description: Agent not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ patch:
+ summary: Update agent
+ description: Update an agent's name or paused state.
+ operationId: updateAgent
+ tags:
+ - Agent Management
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentUpdateRequest'
+ responses:
+ '200':
+ description: Agent updated successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Agent'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Agent not found
content:
application/json:
schema:
@@ -7006,17 +6752,16 @@ paths:
schema:
$ref: '#/components/schemas/Error500'
delete:
- summary: Delete agent external account
- description: |
- Delete an external account belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
- operationId: agentDeleteExternalAccount
+ summary: Delete agent
+ description: Permanently delete an agent. Connected agent software will lose access immediately.
+ operationId: deleteAgent
tags:
- - Agent Operations
+ - Agent Management
security:
- - AgentAuth: []
+ - BasicAuth: []
responses:
'204':
- description: External account deleted successfully
+ description: Agent deleted successfully
'401':
description: Unauthorized
content:
@@ -7024,7 +6769,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: External account not found
+ description: Agent not found
content:
application/json:
schema:
@@ -7035,18 +6780,21 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /cards:
- post:
- summary: Issue a card
+ /agents/{agentId}/policy:
+ parameters:
+ - name: agentId
+ in: path
+ description: System-generated unique agent identifier
+ required: true
+ schema:
+ type: string
+ patch:
+ summary: Update agent policy
description: |
- Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`.
-
- If any funding source is an Embedded Wallet internal account, the cardholder must authorize Grid to sign Spark token transactions for that card funding source by completing the delegated-key creation flow with `POST /auth/delegated-keys`. Until an active delegated key exists for that funding source, Authorization Decisioning cannot use it to fund card transactions.
-
- New cards start in `state: "PROCESSING"` while the card issuer provisions the card. The `card.state_change` webhook fires on each state transition, including the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails).
- operationId: createCard
+ Partially update an agent's policy. Only provided fields will be updated; omitted fields retain their current values. Policy changes take effect immediately.
+ operationId: updateAgentPolicy
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
requestBody:
@@ -7054,25 +6802,16 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/CardCreateRequest'
- examples:
- virtualCard:
- summary: Issue a virtual card with one funding source
- value:
- cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCardId: card-emp-aary-001
- form: VIRTUAL
- fundingSources:
- - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ $ref: '#/components/schemas/AgentPolicyUpdateRequest'
responses:
- '201':
- description: Card created successfully. Newly-created cards start in `PROCESSING` while the issuer provisions them. Cards funded by an Embedded Wallet internal account also require an active delegated key for that funding source before Authorization Decisioning can use it.
+ '200':
+ description: Agent policy updated successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/Card'
+ $ref: '#/components/schemas/Agent'
'400':
- description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters.
+ description: Bad request
content:
application/json:
schema:
@@ -7083,86 +6822,44 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
+ '404':
+ description: Agent not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Card issuance is not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error501'
- get:
- summary: List cards
+ $ref: '#/components/schemas/Error500'
+ /agents/{agentId}/device-codes:
+ parameters:
+ - name: agentId
+ in: path
+ description: System-generated unique agent identifier
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Regenerate a device code
description: |
- Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller.
- operationId: listCards
+ Generate a new device code for an existing agent. Use this when the original device code has expired before being redeemed, or when the agent software needs to be reinstalled. Any previously issued unredeemed device codes for this agent are invalidated.
+ operationId: regenerateAgentDeviceCode
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
- parameters:
- - name: cardholderId
- in: query
- description: Filter by cardholder (customer) id.
- required: false
- schema:
- type: string
- - name: accountId
- in: query
- description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id.
- required: false
- schema:
- type: string
- - name: platformCardId
- in: query
- description: Filter by platform-specific card identifier.
- required: false
- schema:
- type: string
- - name: state
- in: query
- description: Filter by card state.
- required: false
- schema:
- $ref: '#/components/schemas/CardState'
- - name: limit
- in: query
- description: Maximum number of results to return (default 20, max 100)
- required: false
- schema:
- type: integer
- minimum: 1
- maximum: 100
- default: 20
- - name: cursor
- in: query
- description: Cursor for pagination (returned from previous request)
- required: false
- schema:
- type: string
- - name: sortOrder
- in: query
- description: Order to sort results in
- required: false
- schema:
- type: string
- enum:
- - asc
- - desc
- default: desc
responses:
- '200':
- description: Successful operation
+ '201':
+ description: New device code generated successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/CardListResponse'
+ $ref: '#/components/schemas/AgentDeviceCode'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request
content:
application/json:
schema:
@@ -7173,41 +6870,62 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '500':
- description: Internal service error
+ '404':
+ description: Agent not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: Conflict - Agent already has an active connection and cannot regenerate a device code
content:
application/json:
schema:
- $ref: '#/components/schemas/Error501'
- /cards/{id}:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /agents/{agentId}/actions/{actionId}/approve:
parameters:
- - name: id
+ - name: agentId
in: path
- description: System-generated unique card identifier
+ description: System-generated unique agent identifier
required: true
schema:
type: string
- get:
- summary: Get a card
- description: Retrieve a card by its system-generated id. To display the card's full PAN, CVV, and expiry to the cardholder, request a reveal with `POST /cards/{id}/reveal` — the card resource itself never carries the reveal URL.
- operationId: getCardById
+ - name: actionId
+ in: path
+ description: Unique identifier of the agent action to approve
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Approve an agent action
+ description: |
+ Approve a pending agent action, allowing Grid to proceed with execution. The action must have status `PENDING_APPROVAL`. Once approved, Grid executes the underlying operation (quote execution or transfer) and the action transitions to `APPROVED`.
+ For `EXECUTE_QUOTE` actions, note that the underlying quote may have expired between submission and approval — in that case the action will transition to `FAILED` instead.
+ This endpoint is called by the platform's backend using platform credentials, not by the agent itself.
+ operationId: approveAgentAction
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
responses:
'200':
- description: Successful operation
+ description: Action approved successfully. Returns the updated AgentAction.
content:
application/json:
schema:
- $ref: '#/components/schemas/Card'
+ $ref: '#/components/schemas/AgentAction'
+ '400':
+ description: Bad request - Action cannot be approved
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
description: Unauthorized
content:
@@ -7215,128 +6933,80 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Card not found
+ description: Agent or action not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
- '500':
- description: Internal service error
+ '409':
+ description: Conflict - Action is not pending approval or has already been processed
content:
application/json:
schema:
- $ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error501'
- patch:
- summary: Update a card
+ $ref: '#/components/schemas/Error500'
+ /agents/{agentId}/actions/{actionId}/reject:
+ parameters:
+ - name: agentId
+ in: path
+ description: System-generated unique agent identifier
+ required: true
+ schema:
+ type: string
+ - name: actionId
+ in: path
+ description: Unique identifier of the agent action to reject
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Reject an agent action
description: |
- Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied.
-
- - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`.
- - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`.
-
- Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`):
-
- 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
-
- 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`.
-
- Effects:
- - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed.
- - `state: ACTIVE`: normal authorization behavior resumes.
- - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys.
- - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding.
-
- The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated.
- operationId: updateCardById
+ Reject a pending agent action, preventing execution. The action must have status `PENDING_APPROVAL`. Once rejected, the action transitions to `REJECTED` and the underlying operation is not executed.
+ This endpoint is called by the platform's backend using platform credentials, not by the agent itself.
+ operationId: rejectAgentAction
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
- parameters:
- - name: Grid-Wallet-Signature
- in: header
- required: false
- description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call.
- schema:
- type: string
- example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
- - name: Request-Id
- in: header
- required: false
- description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
- schema:
- type: string
- example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
requestBody:
- required: true
+ required: false
content:
application/json:
schema:
- $ref: '#/components/schemas/CardUpdateRequest'
- examples:
- freeze:
- summary: Freeze an active card
- value:
- state: FROZEN
- unfreeze:
- summary: Unfreeze a frozen card
- value:
- state: ACTIVE
- updateFundingSources:
- summary: Replace the card's bound funding sources
- value:
- fundingSources:
- - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
- freezeAndUpdateSources:
- summary: Freeze the card and replace its funding sources in one call
- value:
- state: FROZEN
- fundingSources:
- - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
- close:
- summary: Permanently close the card
- value:
- state: CLOSED
+ $ref: '#/components/schemas/AgentActionRejectRequest'
responses:
'200':
- description: Signed retry accepted. Returns the updated card.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Card'
- '202':
- description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry.
+ description: Action rejected successfully. Returns the updated AgentAction.
content:
application/json:
schema:
- $ref: '#/components/schemas/SignedRequestChallenge'
+ $ref: '#/components/schemas/AgentAction'
'400':
- description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters.
+ description: Bad request - Action cannot be rejected
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge.
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Card not found
+ description: Agent or action not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error404'
'409':
- description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.'
+ description: Conflict - Action is not pending approval or has already been processed
content:
application/json:
schema:
@@ -7347,54 +7017,38 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error501'
- /cards/{id}/reveal:
+ /agents/device-codes/{code}/status:
parameters:
- - name: id
+ - name: code
in: path
- description: System-generated unique card identifier
+ description: The device code to check
required: true
schema:
type: string
- post:
- summary: Reveal card details
- description: |-
- Mint a signed, short-lived URL for the card processor's iframe that displays the card's full PAN, CVV, and expiry to the cardholder. This is the only way to obtain a reveal URL — the `Card` resource never carries one.
-
- Request the reveal right before rendering the iframe and render the returned `panEmbedUrl` immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log the URL — it is a bearer secret for the full card details. The card data renders inside the processor's iframe and never crosses Grid's or your servers.
-
- Every reveal is audit-logged with the requesting actor.
- operationId: revealCard
+ get:
+ summary: Get device code status
+ description: |
+ Check whether a device code has been redeemed. Use this to poll for agent installation completion after creating an agent.
+ operationId: getAgentDeviceCodeStatus
tags:
- - Cards
+ - Agent Management
security:
- BasicAuth: []
responses:
'200':
- description: Reveal URL minted.
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/CardRevealResponse'
+ $ref: '#/components/schemas/AgentDeviceCodeStatusResponse'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden. The session has no attributable actor to audit the reveal against (for example, an impersonated dashboard session).
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Card not found
+ description: Device code not found
content:
application/json:
schema:
@@ -7405,90 +7059,38 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- '501':
- description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error501'
- /sandbox/cards/{id}/simulate/authorization:
- post:
- summary: Simulate a card authorization
- description: |
- Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip.
-
- The decisioning outcome is controlled by the last three characters of `merchant.descriptor`:
-
- | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved |
-
- Production returns `404` on this path.
- operationId: sandboxSimulateCardAuthorization
- tags:
- - Sandbox
- security:
- - BasicAuth: []
- parameters:
- - name: id
- in: path
- required: true
- description: The id of the card to simulate an authorization against.
- schema:
- type: string
- example: Card:019542f5-b3e7-1d02-0000-000000000010
- requestBody:
+ /agents/device-codes/{code}/redeem:
+ parameters:
+ - name: code
+ in: path
+ description: The device code to redeem
required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SandboxCardAuthorizationRequest'
- examples:
- coffeeAuth:
- summary: Approved $12.50 auth at a coffee shop
- value:
- amount: 1250
- currency:
- code: USD
- merchant:
- descriptor: BLUE BOTTLE COFFEE SF
- mcc: '5814'
- country: US
- declinedInsufficientFunds:
- summary: Declined — insufficient funds (descriptor suffix `002`)
- value:
- amount: 50000
- currency:
- code: USD
- merchant:
- descriptor: AMAZON RETAIL US-002
- mcc: '5942'
- country: US
+ schema:
+ type: string
+ post:
+ summary: Redeem device code
+ description: |
+ Redeem a device code to obtain agent credentials. This endpoint is called by the agent software during installation. On success, returns a Bearer access token that the agent uses for all subsequent API calls. The token is returned only once and must be stored securely.
+ This endpoint does not require platform authentication — the device code itself serves as proof of authorization.
+ operationId: redeemAgentDeviceCode
+ tags:
+ - Agent Management
+ security: []
responses:
'200':
- description: Simulated authorization processed. Returns the resulting card transaction.
+ description: Device code redeemed successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/CardTransaction'
+ $ref: '#/components/schemas/AgentDeviceCodeRedeemResponse'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request (e.g., code already redeemed or expired)
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
- '401':
- description: Unauthorized
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error403'
'404':
- description: Card not found (also returned in production for this path)
+ description: Device code not found
content:
application/json:
schema:
@@ -7499,54 +7101,99 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/cards/{id}/simulate/clearing:
- post:
- summary: Simulate a card clearing
+ /agents/me/transactions:
+ get:
+ summary: List agent transactions
description: |
- Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment.
-
- - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth).
- - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted.
- - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases).
-
- Production returns `404` on this path.
- operationId: sandboxSimulateCardClearing
+ Retrieve a paginated list of transactions for the authenticated agent's customer. Results are automatically scoped to the agent's associated customer — no customer filter is needed or accepted.
+ operationId: agentListTransactions
tags:
- - Sandbox
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
parameters:
- - name: id
- in: path
- required: true
- description: The id of the card the clearing applies to.
+ - name: accountIdentifier
+ in: query
+ description: Filter by account identifier (matches either sender or receiver)
+ required: false
schema:
type: string
- example: Card:019542f5-b3e7-1d02-0000-000000000010
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SandboxCardClearingRequest'
- examples:
- tipOnTopClearing:
- summary: Clearing larger than auth — exercises post-hoc pull
- value:
- cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
- amount: 1500
- authorizationExpiry:
- summary: Clearing of 0 — exercises authorization expiry
- value:
- cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
- amount: 0
+ - name: senderAccountIdentifier
+ in: query
+ description: Filter by sender account identifier
+ required: false
+ schema:
+ type: string
+ - name: receiverAccountIdentifier
+ in: query
+ description: Filter by receiver account identifier
+ required: false
+ schema:
+ type: string
+ - name: status
+ in: query
+ description: Filter by transaction status
+ required: false
+ schema:
+ $ref: '#/components/schemas/TransactionStatus'
+ - name: type
+ in: query
+ description: Filter by transaction type
+ required: false
+ schema:
+ $ref: '#/components/schemas/TransactionType'
+ - name: reference
+ in: query
+ description: Filter by reference
+ required: false
+ schema:
+ type: string
+ - name: startDate
+ in: query
+ description: Filter by start date (inclusive) in ISO 8601 format
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: endDate
+ in: query
+ description: Filter by end date (inclusive) in ISO 8601 format
+ required: false
+ schema:
+ type: string
+ format: date-time
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
responses:
'200':
- description: Simulated clearing processed. Returns the updated card transaction.
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/CardTransaction'
+ $ref: '#/components/schemas/TransactionListResponse'
'400':
description: Bad request - Invalid parameters
content:
@@ -7559,14 +7206,44 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '403':
- description: Forbidden - request was made with a production platform token
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error403'
+ $ref: '#/components/schemas/Error500'
+ /agents/me/transactions/{transactionId}:
+ parameters:
+ - name: transactionId
+ in: path
+ description: Unique identifier of the transaction
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get agent transaction by ID
+ description: |
+ Retrieve a specific transaction belonging to the authenticated agent's customer. Returns 404 if the transaction exists but belongs to a different customer.
+ operationId: agentGetTransaction
+ tags:
+ - Agent Operations
+ security:
+ - AgentAuth: []
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TransactionOneOf'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
'404':
- description: Card or card transaction not found
+ description: Transaction not found
content:
application/json:
schema:
@@ -7577,47 +7254,41 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /sandbox/cards/{id}/simulate/return:
+ /agents/me/quotes:
post:
- summary: Simulate a card return
+ summary: Create a transfer quote
description: |
- Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund).
-
- Production returns `404` on this path.
- operationId: sandboxSimulateCardReturn
+ Generate a quote for a cross-currency transfer on behalf of the authenticated agent's customer. Accounts referenced in the request must belong to the agent's customer. Requires the CREATE_QUOTES permission in the agent's policy.
+ If the agent's defaultExecutionMode is APPROVAL_REQUIRED, or the quote amount exceeds the agent's approvalThresholds, the resulting transaction will require explicit approval before funds move.
+ operationId: agentCreateQuote
tags:
- - Sandbox
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
parameters:
- - name: id
- in: path
- required: true
- description: The id of the card the return applies to.
- schema:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
type: string
- example: Card:019542f5-b3e7-1d02-0000-000000000010
+ example:
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/SandboxCardReturnRequest'
- examples:
- fullRefund:
- summary: Full refund of a $15.00 settled transaction
- value:
- cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
- amount: 1500
+ $ref: '#/components/schemas/QuoteRequest'
responses:
- '200':
- description: Simulated return processed. Returns the updated card transaction.
+ '201':
+ description: Transfer quote created successfully
content:
application/json:
schema:
- $ref: '#/components/schemas/CardTransaction'
+ $ref: '#/components/schemas/Quote'
'400':
- description: Bad request - Invalid parameters
+ description: Bad request - Missing or invalid parameters
content:
application/json:
schema:
@@ -7629,13 +7300,61 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'403':
- description: Forbidden - request was made with a production platform token
+ description: Forbidden - Agent policy does not permit this operation
content:
application/json:
schema:
$ref: '#/components/schemas/Error403'
+ '412':
+ description: Counterparty doesn't support UMA version
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error412'
+ '424':
+ description: Counterparty issue
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error424'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /agents/me/quotes/{quoteId}:
+ parameters:
+ - name: quoteId
+ in: path
+ description: ID of the quote to retrieve
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get agent quote by ID
+ description: |
+ Retrieve a quote created by the authenticated agent. Returns 404 if the quote exists but was not created by this agent.
+ operationId: agentGetQuote
+ tags:
+ - Agent Operations
+ security:
+ - AgentAuth: []
+ responses:
+ '200':
+ description: Quote retrieved successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Quote'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
'404':
- description: Card or card transaction not found
+ description: Quote not found
content:
application/json:
schema:
@@ -7646,51 +7365,75 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /stablecoin-provider-accounts:
+ /agents/me/quotes/{quoteId}/execute:
+ parameters:
+ - name: quoteId
+ in: path
+ required: true
+ description: The unique identifier of the quote to execute
+ schema:
+ type: string
+ example: Quote:019542f5-b3e7-1d02-0000-000000000001
post:
- summary: Link a stablecoin provider account
+ summary: Execute a quote
description: |
- Link provider API credentials for the authenticated platform. Provider credentials are account-scoped and can be reused for multiple stablecoins. Currently, the only supported provider is `BRALE`; the provider environment is derived from the authenticated Grid platform mode.
- operationId: linkStablecoinProviderAccount
+ Execute a quote created by the authenticated agent. Requires the EXECUTE_QUOTES permission in the agent's policy.
+ If the agent's policy requires approval for this amount (based on execution mode or approval thresholds), the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
+ Once executed, the quote cannot be cancelled.
+ operationId: agentExecuteQuote
tags:
- - Stablecoins
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
parameters:
- name: Idempotency-Key
in: header
- description: Idempotency key for retrying this request safely. Replays return the prior result; conflicting payloads are rejected.
- required: true
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
schema:
type: string
- maxLength: 255
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/StablecoinProviderAccountLinkRequest'
+ example:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Full Grid wallet signature over the `payloadToSign` returned in the quote's `paymentInstructions[].accountOrWalletInfo` entry, produced with the session private key of a verified authentication credential on the source Embedded Wallet. Required when the quote's source is an internal account of type `EMBEDDED_WALLET`; ignored for other source types.
+ schema:
+ type: string
+ example: eyJwdWJsaWNLZXkiOiIwMmExYjIuLi4iLCJzY2hlbWUiOiJTSUdOQVRVUkVfU0NIRU1FX1RLX0FQSV9QMjU2Iiwic2lnbmF0dXJlIjoiMzA0NTAyMjEwMC4uLiJ9
responses:
- '201':
- description: Stablecoin provider account linked
+ '200':
+ description: 'Action submitted successfully. If the agent''s policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated. Note: if approval is required, the underlying quote may expire before the platform approves — in that case the action will transition to `FAILED`.'
content:
application/json:
schema:
- $ref: '#/components/schemas/StablecoinProviderAccount'
+ $ref: '#/components/schemas/AgentAction'
'400':
- description: Bad request
+ description: Bad request - Quote cannot be executed
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized
+ description: Unauthorized. Also returned when the quote's source is an internal account of type `EMBEDDED_WALLET` and the provided `Grid-Wallet-Signature` header is missing, malformed, or does not match the quote's `payloadToSign`.
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Quote not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
'409':
- description: Conflict
+ description: Conflict - Quote already executed, expired, or in invalid state
content:
application/json:
schema:
@@ -7701,25 +7444,23 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
+ /agents/me/actions:
get:
- summary: List stablecoin provider account links
- description: Retrieve stablecoin provider account links for the authenticated platform.
- operationId: listStablecoinProviderAccounts
+ summary: List agent's own actions
+ description: |
+ Retrieve a paginated list of actions submitted by the authenticated agent. Use this to poll for approval decisions after submitting an action that requires approval.
+ operationId: agentListActions
tags:
- - Stablecoins
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
parameters:
- - name: provider
- in: query
- required: false
- schema:
- $ref: '#/components/schemas/StablecoinProvider'
- name: status
in: query
+ description: Filter by action status
required: false
schema:
- $ref: '#/components/schemas/StablecoinProviderAccountStatus'
+ $ref: '#/components/schemas/AgentActionStatus'
- name: limit
in: query
description: Maximum number of results to return (default 20, max 100)
@@ -7741,13 +7482,7 @@ paths:
content:
application/json:
schema:
- $ref: '#/components/schemas/StablecoinProviderAccountListResponse'
- '400':
- description: Bad request - Invalid parameters
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/AgentActionListResponse'
'401':
description: Unauthorized
content:
@@ -7760,29 +7495,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
- /stablecoin-provider-accounts/{stablecoinProviderAccountId}:
+ /agents/me/actions/{actionId}:
parameters:
- - name: stablecoinProviderAccountId
+ - name: actionId
in: path
- description: System-generated stablecoin provider account link identifier
+ description: Unique identifier of the agent action
required: true
schema:
type: string
get:
- summary: Get a stablecoin provider account link
- description: Retrieve a stablecoin provider account link by id for the authenticated platform.
- operationId: getStablecoinProviderAccount
+ summary: Get an agent action
+ description: |
+ Retrieve a specific action submitted by the authenticated agent. Poll this endpoint after submitting an action that requires approval to check whether it has been approved, rejected, or has failed.
+ operationId: agentGetAction
tags:
- - Stablecoins
+ - Agent Operations
security:
- - BasicAuth: []
+ - AgentAuth: []
responses:
'200':
description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/StablecoinProviderAccount'
+ $ref: '#/components/schemas/AgentAction'
'401':
description: Unauthorized
content:
@@ -7790,7 +7526,7 @@ paths:
schema:
$ref: '#/components/schemas/Error401'
'404':
- description: Stablecoin provider account link not found
+ description: Action not found
content:
application/json:
schema:
@@ -7801,580 +7537,316 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
-webhooks:
- agent-action:
+ /agents/me/transfer-in:
post:
- summary: Agent action pending approval webhook
+ summary: Create a transfer-in
description: |
- Fired when an agent submits an action that requires platform approval before Grid will execute it. Use this to send a push notification to the customer so they can review and approve or reject the action in your app.
- This endpoint should be implemented by clients of the Grid API.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- The payload contains the full `AgentAction` — including the embedded quote or transfer details — so you can render the approval UI without a second API call. Approve or reject via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`.
- operationId: agentActionWebhook
+ Transfer funds from an external account to an internal account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy.
+ If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
+ This endpoint should only be used for external account sources with pull functionality (e.g. ACH Pull). Otherwise, use the payment instructions on the internal account to deposit funds.
+ operationId: agentCreateTransferIn
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
+ - AgentAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/AgentActionWebhook'
+ $ref: '#/components/schemas/TransferInRequest'
examples:
- pendingApproval:
- summary: Agent action pending approval
+ transferIn:
+ summary: Transfer from external to internal account
value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000020
- type: AGENT_ACTION.PENDING_APPROVAL
- timestamp: '2025-10-03T15:00:00Z'
- data:
- id: AgentAction:019542f5-b3e7-1d02-0000-000000000099
- agentId: Agent:019542f5-b3e7-1d02-0000-000000000042
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000010
- platformCustomerId: user-a1b2c3
- status: PENDING_APPROVAL
- type: EXECUTE_QUOTE
- quote:
- id: Quote:019542f5-b3e7-1d02-0000-000000000006
- status: PENDING
- expiresAt: '2025-10-03T15:00:30Z'
- createdAt: '2025-10-03T15:00:00Z'
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- sendingCurrency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- receivingCurrency:
- code: INR
- name: Indian Rupee
- symbol: ₹
- decimals: 2
- totalSendingAmount: 50000
- totalReceivingAmount: 4625000
- exchangeRate: 92.5
- feesIncluded: 250
- transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000099
- createdAt: '2025-10-03T15:00:00Z'
- updatedAt: '2025-10-03T15:00:00Z'
+ source:
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ amount: 12550
responses:
- '200':
- description: Webhook received and acknowledged.
+ '201':
+ description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentAction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ '403':
+ description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- incoming-payment:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /agents/me/transfer-out:
post:
- summary: Incoming payment webhook and approval mechanism
+ summary: Create a transfer-out
description: |
- Webhook that is called when an incoming payment is received by a customer's UMA address.
- This endpoint should be implemented by clients of the Grid API.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- ### Payment Approval Flow
- When a transaction has `status: "PENDING"`, this webhook serves as an approval mechanism:
-
- 1. The client should check the `counterpartyInformation` against their requirements
- 2. To APPROVE the payment synchronously, return a 200 OK response
- 3. To REJECT the payment, return a 403 Forbidden response with an Error object
- 4. To request more information, return a 422 Unprocessable Entity with specific missing fields
- 5. To process the payment asynchronously, return a 202 Accepted response and then call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint within 5 seconds. Note that synchronous approval/rejection is preferred where possible.
-
- The Grid system will proceed or cancel the payment based on your response.
-
- For transactions with other statuses (COMPLETED, FAILED, REFUNDED), this webhook is purely informational.
- operationId: incomingPaymentWebhook
+ Transfer funds from an internal account to an external account for the authenticated agent's customer. Accounts must belong to the agent's customer. Requires the CREATE_TRANSFERS permission in the agent's policy.
+ If the agent's policy requires approval for this amount, the transaction will be created in a pending state and must be approved by the platform via `POST /agents/{agentId}/actions/{actionId}/approve`.
+ operationId: agentCreateTransferOut
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
+ - AgentAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ required: false
+ description: |
+ A unique identifier for the request. If the same key is sent multiple times, the server will return the same response as the first request.
+ schema:
+ type: string
+ example: 550e8400-e29b-41d4-a716-446655440000
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingPaymentWebhook'
+ $ref: '#/components/schemas/TransferOutRequest'
examples:
- pendingPayment:
- summary: Pending payment example requiring approval
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000007
- type: INCOMING_PAYMENT.PENDING
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000005
- status: PENDING
- type: INCOMING
- direction: CREDIT
- destination:
- destinationType: UMA_ADDRESS
- umaAddress: $recipient@uma.domain
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- senderUmaAddress: $sender@external.domain
- receiverUmaAddress: $recipient@uma.domain
- receivedAmount:
- amount: 50000
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- counterpartyInformation:
- FULL_NAME: John Sender
- BIRTH_DATE: '1985-06-15'
- NATIONALITY: US
- reconciliationInstructions:
- reference: REF-123456789
- requestedReceiverCustomerInfoFields:
- - name: NATIONALITY
- mandatory: true
- - name: POSTAL_ADDRESS
- mandatory: false
- incomingCompletedPayment:
- summary: Completed payment notification
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000007
- type: INCOMING_PAYMENT.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000005
- status: COMPLETED
- type: INCOMING
- direction: CREDIT
- destination:
- destinationType: UMA_ADDRESS
- umaAddress: $recipient@uma.domain
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- senderUmaAddress: $sender@external.domain
- receiverUmaAddress: $recipient@uma.domain
- receivedAmount:
- amount: 50000
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- settledAt: '2025-08-15T14:30:00Z'
- createdAt: '2025-08-15T14:25:18Z'
- description: Payment for services
- reconciliationInstructions:
- reference: REF-123456789
- incomingCompletedCryptoPayment:
- summary: Completed payment funded from an external crypto wallet
+ transferOut:
+ summary: Transfer from internal to external account
value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000009
- type: INCOMING_PAYMENT.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000006
- status: COMPLETED
- type: INCOMING
- direction: CREDIT
- source:
- sourceType: REALTIME_FUNDING
- currency: USDC
- onChainTransaction:
- transactionHash: 7RJWhvQBQPEjJmki5fhBboGBWRJhmcFkMvrr4Fu3tMSJ5EdynMEiYSyiWAH9GpcbHpeUzeSQF9ZY6q4x8AhBskUf
- network: SOLANA
- destination:
- destinationType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- receivedAmount:
- amount: 100000
- currency:
- code: USDC
- name: USD Coin
- symbol: ''
- decimals: 6
- settledAt: '2025-08-15T14:30:00Z'
- createdAt: '2025-08-15T14:25:18Z'
- description: USDC deposit from self-custody wallet
+ source:
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ destination:
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ amount: 12550
responses:
- '200':
- description: |
- Webhook received successfully.
- For PENDING transactions, this indicates approval to proceed with the payment.
- If `requestedReceiverCustomerInfoFields` were present in the webhook request, the corresponding fields for the recipient must be included in this response in the `receiverCustomerInfo` object.
+ '201':
+ description: Action submitted successfully. If the agent's policy requires approval, the returned `AgentAction` will have status `PENDING_APPROVAL` and no `transaction` yet. If the policy permits automatic execution, status will be `APPROVED` and `transaction` will be populated.
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingPaymentWebhookResponse'
- '202':
- description: |
- Webhook received and will be processed asynchronously. The synchronous 200 response should be preferred where possible. This asycnhronous path should only be used in
- cases where the platform's architecture requires async (but still very quick) processing before approving or rejecting the payment.
- The platform must call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint to approve or reject the payment within 5 seconds or the payment will be automatically rejected.
+ $ref: '#/components/schemas/AgentAction'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'403':
- description: |
- Forbidden - Payment rejected by the client.
- Only applicable for PENDING transactions.
+ description: Forbidden - Agent policy does not permit this operation or spending limit exceeded
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Account not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- '422':
- description: |
- Unprocessable Entity - Additional counterparty information required.
- Only applicable for PENDING transactions.
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/IncomingPaymentWebhookUnprocessableResponse'
- outgoing-payment:
- post:
- summary: Outgoing payment status webhook
+ $ref: '#/components/schemas/Error500'
+ /agents/me/internal-accounts:
+ get:
+ summary: List agent's internal accounts
description: |
- Webhook that is called when an outgoing payment's status changes.
- This endpoint should be implemented by clients of the Grid API.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded.
- operationId: outgoingPaymentWebhook
+ Retrieve the internal accounts belonging to the customer this agent operates on behalf of. Use this to discover available source accounts for transfers and quotes, and to verify which accounts are accessible under the agent's `accountRestrictions` policy.
+ operationId: agentListInternalAccounts
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/OutgoingPaymentWebhook'
- examples:
- outgoingCompletedPayment:
- summary: Completed outgoing payment
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000007
- type: OUTGOING_PAYMENT.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000005
- status: COMPLETED
- type: OUTGOING
- direction: DEBIT
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- senderUmaAddress: $sender@uma.domain
- receiverUmaAddress: $recipient@external.domain
- sentAmount:
- amount: 10550
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- receivedAmount:
- amount: 9706
- currency:
- code: EUR
- name: Euro
- symbol: €
- decimals: 2
- exchangeRate: 0.92
- quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006
- settledAt: '2025-08-15T14:30:00Z'
- createdAt: '2025-08-15T14:25:18Z'
- description: 'Payment for invoice #1234'
- paymentInstructions: []
- rateDetails:
- counterpartyMultiplier: 1.08
- counterpartyFixedFee: 10
- gridApiMultiplier: 0.925
- gridApiFixedFee: 10
- gridApiVariableFeeRate: 0.003
- gridApiVariableFeeAmount: 30
- outgoingCompletedCryptoPayment:
- summary: Completed crypto payout to an external wallet
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000008
- type: OUTGOING_PAYMENT.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000009
- status: COMPLETED
- type: OUTGOING
- direction: DEBIT
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:c34dcbd6-dced-4ec4-b756-3c3a9ea3d789
- onChainTransaction:
- transactionHash: h82pJGF9p7kpzb6eU326EFZf2cDnimbTFVeJtx1qtBmUNJAEqN76R7PwPfHt3oWb8R6cKvhgyxQdDn53jFrK6wFx
- network: SOLANA
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- sentAmount:
- amount: 100000
- currency:
- code: USDC
- name: USD Coin
- symbol: ''
- decimals: 6
- receivedAmount:
- amount: 100000
- currency:
- code: USDC
- name: USD Coin
- symbol: ''
- decimals: 6
- quoteId: Quote:019542f5-b3e7-1d02-0000-000000000010
- settledAt: '2025-08-15T14:30:00Z'
- createdAt: '2025-08-15T14:25:18Z'
- description: USDC withdrawal to self-custody wallet
- paymentInstructions: []
- failedPayment:
- summary: Failed outgoing payment
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000007
- type: OUTGOING_PAYMENT.FAILED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Transaction:019542f5-b3e7-1d02-0000-000000000005
- status: FAILED
- type: OUTGOING
- direction: DEBIT
- source:
- sourceType: ACCOUNT
- accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
- destination:
- destinationType: ACCOUNT
- accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
- customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
- platformCustomerId: 18d3e5f7b4a9c2
- senderUmaAddress: $sender@uma.domain
- receiverUmaAddress: $recipient@external.domain
- sentAmount:
- amount: 10550
- currency:
- code: USD
- name: United States Dollar
- symbol: $
- decimals: 2
- createdAt: '2025-08-15T14:25:18Z'
- quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006
- failureReason: QUOTE_EXECUTION_FAILED
+ - AgentAuth: []
+ parameters:
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
+ schema:
+ type: string
+ - name: type
+ in: query
+ description: Filter by internal account type. Use `EMBEDDED_WALLET` to find the self-custodial wallet provisioned for the customer, or `INTERNAL_FIAT` / `INTERNAL_CRYPTO` for platform-managed holding accounts.
+ required: false
+ schema:
+ $ref: '#/components/schemas/InternalAccountType'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Webhook received successfully
- '400':
- description: Bad request
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/InternalAccountListResponse'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- test-webhook:
- post:
- summary: Test webhook for integration verification
+ $ref: '#/components/schemas/Error500'
+ /agents/me/external-accounts:
+ get:
+ summary: List agent external accounts
description: |
- Webhook that is sent once to verify your webhook endpoint is correctly set up.
- This is sent when you configure or update your platform settings with a webhook URL.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by the Grid API.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- This webhook is purely for testing your endpoint integration and signature verification.
- operationId: testWebhook
+ Retrieve a paginated list of external accounts belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
+ operationId: agentListExternalAccounts
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TestWebhookRequest'
- examples:
- testWebhook:
- summary: Test webhook example
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000001
- type: TEST
- timestamp: '2025-08-15T14:32:00Z'
- data: {}
+ - AgentAuth: []
+ parameters:
+ - name: currency
+ in: query
+ description: Filter by currency code
+ required: false
+ schema:
+ type: string
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
responses:
'200':
- description: Webhook received successfully. This confirms your webhook endpoint is properly configured.
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExternalAccountListResponse'
'400':
- description: Bad request
+ description: Bad request - Invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ '500':
+ description: Internal service error
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- bulk-upload:
+ $ref: '#/components/schemas/Error500'
post:
- summary: Bulk upload status webhook
+ summary: Add an external account
description: |
- Webhook that is called when a bulk customer upload job completes or fails.
- This endpoint should be implemented by clients of the Grid API.
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
-
- This webhook is sent when a bulk upload job completes or fails, providing detailed information about the results.
- operationId: bulkUploadWebhook
+ Register a new external bank account or wallet for the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy. The `customerId` field is optional and will be inferred from the agent's associated customer if omitted.
+ operationId: agentCreateExternalAccount
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
+ - AgentAuth: []
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/BulkUploadWebhook'
+ $ref: '#/components/schemas/ExternalAccountCreateRequest'
examples:
- completedUpload:
- summary: Successful bulk upload completion
+ usBankAccount:
+ summary: Create external US bank account
value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000008
- type: BULK_UPLOAD.COMPLETED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Job:019542f5-b3e7-1d02-0000-000000000006
- status: COMPLETED
- progress:
- total: 5000
- processed: 5000
- successful: 5000
- failed: 0
- errors: []
- failedUpload:
- summary: Failed bulk upload
+ currency: USD
+ accountInfo:
+ accountType: USD_ACCOUNT
+ accountNumber: '12345678901'
+ routingNumber: '123456789'
+ bankAccountType: CHECKING
+ bankName: Chase Bank
+ beneficiary:
+ beneficiaryType: INDIVIDUAL
+ fullName: John Doe
+ birthDate: '1990-01-15'
+ nationality: US
+ address:
+ line1: 123 Main Street
+ city: San Francisco
+ state: CA
+ postalCode: '94105'
+ country: US
+ sparkWallet:
+ summary: Create external Spark wallet
value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000008
- type: BULK_UPLOAD.FAILED
- timestamp: '2025-08-15T14:32:00Z'
- data:
- id: Job:019542f5-b3e7-1d02-0000-000000000006
- status: FAILED
- progress:
- total: 5000
- processed: 5000
- successful: 0
- failed: 5000
- errors:
- - correlationId: row_1
- error:
- code: invalid_csv_format
- message: Invalid CSV format
- details:
- reason: missing_required_column
- column: umaAddress
+ currency: BTC
+ accountInfo:
+ accountType: SPARK_WALLET
+ address: spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu
responses:
- '200':
- description: Webhook received successfully
+ '201':
+ description: External account created successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ExternalAccount'
'400':
description: Bad request
content:
@@ -8382,121 +7854,1557 @@ webhooks:
schema:
$ref: '#/components/schemas/Error400'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
'409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ description: Conflict - External account already exists
content:
application/json:
schema:
$ref: '#/components/schemas/Error409'
- invitation-claimed:
- post:
- summary: Invitation claimed webhook
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /agents/me/external-accounts/{externalAccountId}:
+ parameters:
+ - name: externalAccountId
+ in: path
+ description: System-generated unique external account identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get agent external account by ID
description: |
- Webhook that is called when an invitation is claimed by a customer.
- This endpoint should be implemented by platform clients of the Grid API.
-
- When a customer claims an invitation, this webhook is triggered to notify the platform that:
- 1. The invitation has been successfully claimed
- 2. The invitee UMA address is now associated with the invitation
- 3. The invitation status has changed from PENDING to CLAIMED
-
- This allows platforms to:
- - Track invitation usage and conversion rates
- - Trigger onboarding flows for new customers who joined via invitation
- - Apply referral bonuses or rewards to the inviter
- - Update their UI to reflect the claimed status
-
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
-
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
- operationId: invitationClaimedWebhook
+ Retrieve an external account belonging to the authenticated agent's customer. Returns 404 if the account exists but belongs to a different customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
+ operationId: agentGetExternalAccount
tags:
- - Webhooks
+ - Agent Operations
security:
- - WebhookSignature: []
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/InvitationClaimedWebhook'
- examples:
- claimedInvitation:
- summary: Invitation claimed notification
- value:
- id: Webhook:019542f5-b3e7-1d02-0000-000000000008
- type: INVITATION.CLAIMED
- timestamp: '2025-09-01T15:45:00Z'
- data:
- code: 019542f5
- createdAt: '2025-09-01T14:30:00Z'
- claimedAt: '2025-09-01T15:45:00Z'
- inviterUma: $inviter@uma.domain
- inviteeUma: $invitee@uma.domain
- status: CLAIMED
- url: https://uma.me/i/019542f5
+ - AgentAuth: []
responses:
'200':
- description: Webhook received successfully
- '400':
- description: Bad request
+ description: Successful operation
content:
application/json:
schema:
- $ref: '#/components/schemas/Error400'
+ $ref: '#/components/schemas/ExternalAccount'
'401':
- description: Unauthorized - Signature validation failed
+ description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error401'
- '409':
- description: Conflict - Webhook has already been processed (duplicate id)
+ '404':
+ description: External account not found
content:
application/json:
schema:
- $ref: '#/components/schemas/Error409'
- customer-update:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ delete:
+ summary: Delete agent external account
+ description: |
+ Delete an external account belonging to the authenticated agent's customer. Requires the MANAGE_EXTERNAL_ACCOUNTS permission in the agent's policy.
+ operationId: agentDeleteExternalAccount
+ tags:
+ - Agent Operations
+ security:
+ - AgentAuth: []
+ responses:
+ '204':
+ description: External account deleted successfully
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: External account not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /cards:
post:
- summary: Customer status change
+ summary: Issue a card
description: |
- Webhook that is called when the status of a customer is updated, including KYC and KYB status changes.
- This endpoint should be implemented by clients of the Grid API.
+ Issue a new card for a cardholder. Every card must be bound to at least one funding source at create time. The cardholder must have KYC status `APPROVED` before a card can be issued; otherwise the request is rejected with `CARDHOLDER_KYC_NOT_APPROVED`.
- ### Authentication
- The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
- To verify the signature:
- 1. Get the Grid API public key provided to you during integration
- 2. Decode the base64 signature from the header
- 3. Create a SHA-256 hash of the request body
- 4. Verify the signature using the public key and the hash
+ If any funding source is an Embedded Wallet internal account, the cardholder must authorize Grid to sign Spark token transactions for that card funding source by completing the delegated-key creation flow with `POST /auth/delegated-keys`. Until an active delegated key exists for that funding source, Authorization Decisioning cannot use it to fund card transactions.
- If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
- operationId: customerStatusWebhook
+ New cards start in `state: "PROCESSING"` while the card issuer provisions the card. The `card.state_change` webhook fires on each state transition, including the transition to `ACTIVE` (or to `CLOSED` with `stateReason: "ISSUER_REJECTED"` if provisioning fails).
+ operationId: createCard
tags:
- - Webhooks
+ - Cards
security:
- - WebhookSignature: []
+ - BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
- $ref: '#/components/schemas/CustomerWebhook'
+ $ref: '#/components/schemas/CardCreateRequest'
examples:
- kycApprovedWebhook:
- summary: When an individual customer KYC has been approved
+ virtualCard:
+ summary: Issue a virtual card with one funding source
+ value:
+ cardholderId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCardId: card-emp-aary-001
+ form: VIRTUAL
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ responses:
+ '201':
+ description: Card created successfully. Newly-created cards start in `PROCESSING` while the issuer provisions them. Cards funded by an Embedded Wallet internal account also require an active delegated key for that funding source before Authorization Decisioning can use it.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '400':
+ description: Bad request. Returned with `CARDHOLDER_KYC_NOT_APPROVED` when the cardholder's KYC status is not `APPROVED`, with `FUNDING_SOURCE_INELIGIBLE` when the supplied funding source does not belong to the cardholder or is not denominated in a card-eligible currency, and for general invalid parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Card issuance is not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ get:
+ summary: List cards
+ description: |
+ Retrieve a paginated list of cards. Cards can be filtered by cardholder, bound funding-source internal account, state, and platform-specific card identifier. If no filters are provided, returns all cards visible to the caller.
+ operationId: listCards
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: cardholderId
+ in: query
+ description: Filter by cardholder (customer) id.
+ required: false
+ schema:
+ type: string
+ - name: accountId
+ in: query
+ description: Filter by internal account id. Returns cards whose `fundingSources` array contains the given internal account id.
+ required: false
+ schema:
+ type: string
+ - name: platformCardId
+ in: query
+ description: Filter by platform-specific card identifier.
+ required: false
+ schema:
+ type: string
+ - name: state
+ in: query
+ description: Filter by card state.
+ required: false
+ schema:
+ $ref: '#/components/schemas/CardState'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ - name: sortOrder
+ in: query
+ description: Order to sort results in
+ required: false
+ schema:
+ type: string
+ enum:
+ - asc
+ - desc
+ default: desc
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardListResponse'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ /cards/{id}:
+ parameters:
+ - name: id
+ in: path
+ description: System-generated unique card identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get a card
+ description: Retrieve a card by its system-generated id. To display the card's full PAN, CVV, and expiry to the cardholder, request a reveal with `POST /cards/{id}/reveal` — the card resource itself never carries the reveal URL.
+ operationId: getCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ patch:
+ summary: Update a card
+ description: |
+ Update a card's `state` and / or its bound `fundingSources`. At least one of the two fields must be supplied.
+
+ - `state` transitions are limited to `ACTIVE ⇄ FROZEN` and `ACTIVE | FROZEN → CLOSED`. `CLOSED` is terminal and irreversible. Any other transition returns `409 INVALID_STATE_TRANSITION`.
+ - `fundingSources`, when supplied, fully replaces the card's bound funding sources. Array order determines the priority Authorization Decisioning tries them in. Each id must belong to the cardholder and be denominated in the card's currency; the list must contain at least one source. `fundingSources` cannot be supplied alongside `state: CLOSED`.
+
+ Because both updates are sensitive state changes, this endpoint uses Grid's 202 → signed-retry pattern (same shape as `DELETE /auth/credentials/{id}` and `POST /internal-accounts/{id}/export`):
+
+ 1. Call `PATCH /cards/{id}` with the target fields and no signing headers. The response is `202` with a `payloadToSign`, `requestId`, and `expiresAt`.
+
+ 2. Sign the `payloadToSign` with the session private key of a verified authentication credential on the card's owning internal account and retry with the signature as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `200` with the updated `Card`.
+
+ Effects:
+ - `state: FROZEN`: Authorization Decisioning declines new auths with `CARD_PAUSED`. Existing pulls and in-flight reconciliation continue — freezing does not pause the lifecycle of authorizations that already passed.
+ - `state: ACTIVE`: normal authorization behavior resumes.
+ - `state: CLOSED`: terminal close. The card transitions to `state: "CLOSED"` with `stateReason: "CLOSED_BY_PLATFORM"` and stays in the system for audit and reconciliation. All pending auths reconcile to a terminal state via the existing reconcile primitive. Inbound clearings received after close follow the standard force-post / late-presentment path — Lightspark absorbs the loss if a post-hoc pull on the now-unbound source fails. Funding-source bindings are detached. Refunds already in flight still complete because Lightspark holds the card-reserve keys.
+ - `fundingSources` change: emits `card.funding_source_change` reflecting the new ordered binding.
+
+ The `card.state_change` webhook fires on every successful `state` transition; the `card.funding_source_change` webhook fires whenever `fundingSources` is updated.
+ operationId: updateCardById
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: Grid-Wallet-Signature
+ in: header
+ required: false
+ description: Signature over the `payloadToSign` returned in a prior `202` response, produced with the session private key of a verified authentication credential on the card's owning internal account and base64-encoded. Required on the signed retry; ignored on the initial call.
+ schema:
+ type: string
+ example: MEUCIQDx7k2N0aK4p8f3vR9J6yT5wL1mB0sXnG2hQ4vJ8zYkCgIgZ4rP9dT7eWfU3oM6KjR1qSpNvBwL0tXyA2iG8fH5dE=
+ - name: Request-Id
+ in: header
+ required: false
+ description: The `requestId` returned in a prior `202` response, echoed back on the signed retry so the server can correlate it with the issued challenge. Required on the signed retry; must be paired with `Grid-Wallet-Signature`.
+ schema:
+ type: string
+ example: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardUpdateRequest'
+ examples:
+ freeze:
+ summary: Freeze an active card
+ value:
+ state: FROZEN
+ unfreeze:
+ summary: Unfreeze a frozen card
+ value:
+ state: ACTIVE
+ updateFundingSources:
+ summary: Replace the card's bound funding sources
+ value:
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000003
+ freezeAndUpdateSources:
+ summary: Freeze the card and replace its funding sources in one call
+ value:
+ state: FROZEN
+ fundingSources:
+ - InternalAccount:019542f5-b3e7-1d02-0000-000000000002
+ close:
+ summary: Permanently close the card
+ value:
+ state: CLOSED
+ responses:
+ '200':
+ description: Signed retry accepted. Returns the updated card.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Card'
+ '202':
+ description: Challenge issued. The response contains a `payloadToSign` that must be signed with the session private key of a verified authentication credential on the card's owning internal account, along with a `requestId` that must be echoed back on the retry.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SignedRequestChallenge'
+ '400':
+ description: Bad request. Returned with `FUNDING_SOURCE_INELIGIBLE` when a supplied funding source does not belong to the cardholder or is not denominated in the card's currency, and for general invalid parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending update challenge for this card, or when the `Request-Id` does not match an unexpired pending challenge.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '409':
+ description: 'Conflict. Returned with `INVALID_STATE_TRANSITION` when the requested `state` transition is not one of `ACTIVE ⇄ FROZEN` or `ACTIVE | FROZEN → CLOSED` (e.g. trying to un-freeze a `CLOSED` card); with `CARD_ALREADY_CLOSED` when `state: CLOSED` is requested for a card that is already `CLOSED`; and with `CARD_NOT_MUTABLE` when the card is `CLOSED`.'
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ /cards/{id}/reveal:
+ parameters:
+ - name: id
+ in: path
+ description: System-generated unique card identifier
+ required: true
+ schema:
+ type: string
+ post:
+ summary: Reveal card details
+ description: |-
+ Mint a signed, short-lived URL for the card processor's iframe that displays the card's full PAN, CVV, and expiry to the cardholder. This is the only way to obtain a reveal URL — the `Card` resource never carries one.
+
+ Request the reveal right before rendering the iframe and render the returned `panEmbedUrl` immediately; it expires at `expiresAt` (within minutes). Never store, cache, or log the URL — it is a bearer secret for the full card details. The card data renders inside the processor's iframe and never crosses Grid's or your servers.
+
+ Every reveal is audit-logged with the requesting actor.
+ operationId: revealCard
+ tags:
+ - Cards
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Reveal URL minted.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardRevealResponse'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden. The session has no attributable actor to audit the reveal against (for example, an impersonated dashboard session).
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ '501':
+ description: Not implemented in this environment. Cards are not enabled for every Grid deployment; environments without a configured card issuer return `501 NOT_IMPLEMENTED`.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error501'
+ /sandbox/cards/{id}/simulate/authorization:
+ post:
+ summary: Simulate a card authorization
+ description: |
+ Simulate an inbound card authorization in the sandbox environment. Drives the same internal `authorize` + `reconcile` paths the card issuer would call in production, so platforms can exercise Grid's decisioning + funding-source pull behavior end-to-end without an external network round-trip.
+
+ The decisioning outcome is controlled by the last three characters of `merchant.descriptor`:
+
+ | Suffix | Outcome | | ------ | ------- | | `002` | Decline — `INSUFFICIENT_FUNDS` (the pull on the funding source fails) | | `003` | Decline — `CARD_PAUSED` (intended to verify a frozen card refuses auths) | | `005` | Delayed pull (~30s) — exercises the `PENDING → CONFIRMED` path | | `006` | Pull succeeds but the confirmation event reports `FAILED` — exercises the high-urgency `EXCEPTION` alert | | any other | Approved |
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardAuthorization
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card to simulate an authorization against.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardAuthorizationRequest'
+ examples:
+ coffeeAuth:
+ summary: Approved $12.50 auth at a coffee shop
+ value:
+ amount: 1250
+ currency:
+ code: USD
+ merchant:
+ descriptor: BLUE BOTTLE COFFEE SF
+ mcc: '5814'
+ country: US
+ declinedInsufficientFunds:
+ summary: Declined — insufficient funds (descriptor suffix `002`)
+ value:
+ amount: 50000
+ currency:
+ code: USD
+ merchant:
+ descriptor: AMAZON RETAIL US-002
+ mcc: '5942'
+ country: US
+ responses:
+ '200':
+ description: Simulated authorization processed. Returns the resulting card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card not found (also returned in production for this path)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/clearing:
+ post:
+ summary: Simulate a card clearing
+ description: |
+ Simulate a clearing (settlement) event against an existing `CardTransaction` in the sandbox environment.
+
+ - A clearing `amount` greater than the authorized amount exercises the over-auth post-hoc-pull path (e.g. restaurant tip on top of a 20% over-auth).
+ - A clearing `amount` of `0` exercises the `AUTHORIZATION_EXPIRY` path — the auth expires with no clearing posted.
+ - Suffix-driven outcomes on the parent transaction's id govern whether the post-hoc pull succeeds (use the suffix table from `simulate/authorization` to construct deterministic test cases).
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardClearing
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the clearing applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardClearingRequest'
+ examples:
+ tipOnTopClearing:
+ summary: Clearing larger than auth — exercises post-hoc pull
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ authorizationExpiry:
+ summary: Clearing of 0 — exercises authorization expiry
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 0
+ responses:
+ '200':
+ description: Simulated clearing processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /sandbox/cards/{id}/simulate/return:
+ post:
+ summary: Simulate a card return
+ description: |
+ Simulate a merchant-initiated `RETURN` against an existing settled card transaction in the sandbox environment. Creates a `CardRefund` on the parent and either flips the parent to `REFUNDED` (full refund) or keeps it `SETTLED` with a non-zero `refundedAmount` (partial refund).
+
+ Production returns `404` on this path.
+ operationId: sandboxSimulateCardReturn
+ tags:
+ - Sandbox
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: id
+ in: path
+ required: true
+ description: The id of the card the return applies to.
+ schema:
+ type: string
+ example: Card:019542f5-b3e7-1d02-0000-000000000010
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SandboxCardReturnRequest'
+ examples:
+ fullRefund:
+ summary: Full refund of a $15.00 settled transaction
+ value:
+ cardTransactionId: CardTransaction:019542f5-b3e7-1d02-0000-000000000100
+ amount: 1500
+ responses:
+ '200':
+ description: Simulated return processed. Returns the updated card transaction.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CardTransaction'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: Forbidden - request was made with a production platform token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error403'
+ '404':
+ description: Card or card transaction not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /stablecoin-provider-accounts:
+ post:
+ summary: Link a stablecoin provider account
+ description: |
+ Link provider API credentials for the authenticated platform. Provider credentials are account-scoped and can be reused for multiple stablecoins. Currently, the only supported provider is `BRALE`; the provider environment is derived from the authenticated Grid platform mode.
+ operationId: linkStablecoinProviderAccount
+ tags:
+ - Stablecoins
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: Idempotency-Key
+ in: header
+ description: Idempotency key for retrying this request safely. Replays return the prior result; conflicting payloads are rejected.
+ required: true
+ schema:
+ type: string
+ maxLength: 255
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccountLinkRequest'
+ responses:
+ '201':
+ description: Stablecoin provider account linked
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccount'
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ get:
+ summary: List stablecoin provider account links
+ description: Retrieve stablecoin provider account links for the authenticated platform.
+ operationId: listStablecoinProviderAccounts
+ tags:
+ - Stablecoins
+ security:
+ - BasicAuth: []
+ parameters:
+ - name: provider
+ in: query
+ required: false
+ schema:
+ $ref: '#/components/schemas/StablecoinProvider'
+ - name: status
+ in: query
+ required: false
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccountStatus'
+ - name: limit
+ in: query
+ description: Maximum number of results to return (default 20, max 100)
+ required: false
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 100
+ default: 20
+ - name: cursor
+ in: query
+ description: Cursor for pagination (returned from previous request)
+ required: false
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccountListResponse'
+ '400':
+ description: Bad request - Invalid parameters
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+ /stablecoin-provider-accounts/{stablecoinProviderAccountId}:
+ parameters:
+ - name: stablecoinProviderAccountId
+ in: path
+ description: System-generated stablecoin provider account link identifier
+ required: true
+ schema:
+ type: string
+ get:
+ summary: Get a stablecoin provider account link
+ description: Retrieve a stablecoin provider account link by id for the authenticated platform.
+ operationId: getStablecoinProviderAccount
+ tags:
+ - Stablecoins
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StablecoinProviderAccount'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '404':
+ description: Stablecoin provider account link not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error404'
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error500'
+webhooks:
+ agent-action:
+ post:
+ summary: Agent action pending approval webhook
+ description: |
+ Fired when an agent submits an action that requires platform approval before Grid will execute it. Use this to send a push notification to the customer so they can review and approve or reject the action in your app.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ The payload contains the full `AgentAction` — including the embedded quote or transfer details — so you can render the approval UI without a second API call. Approve or reject via `POST /agents/{agentId}/actions/{actionId}/approve` or `POST /agents/{agentId}/actions/{actionId}/reject`.
+ operationId: agentActionWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AgentActionWebhook'
+ examples:
+ pendingApproval:
+ summary: Agent action pending approval
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000020
+ type: AGENT_ACTION.PENDING_APPROVAL
+ timestamp: '2025-10-03T15:00:00Z'
+ data:
+ id: AgentAction:019542f5-b3e7-1d02-0000-000000000099
+ agentId: Agent:019542f5-b3e7-1d02-0000-000000000042
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000010
+ platformCustomerId: user-a1b2c3
+ status: PENDING_APPROVAL
+ type: EXECUTE_QUOTE
+ quote:
+ id: Quote:019542f5-b3e7-1d02-0000-000000000006
+ status: PENDING
+ expiresAt: '2025-10-03T15:00:30Z'
+ createdAt: '2025-10-03T15:00:00Z'
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ sendingCurrency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ receivingCurrency:
+ code: INR
+ name: Indian Rupee
+ symbol: ₹
+ decimals: 2
+ totalSendingAmount: 50000
+ totalReceivingAmount: 4625000
+ exchangeRate: 92.5
+ feesIncluded: 250
+ transactionId: Transaction:019542f5-b3e7-1d02-0000-000000000099
+ createdAt: '2025-10-03T15:00:00Z'
+ updatedAt: '2025-10-03T15:00:00Z'
+ responses:
+ '200':
+ description: Webhook received and acknowledged.
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ incoming-payment:
+ post:
+ summary: Incoming payment webhook and approval mechanism
+ description: |
+ Webhook that is called when an incoming payment is received by a customer's UMA address.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ ### Payment Approval Flow
+ When a transaction has `status: "PENDING"`, this webhook serves as an approval mechanism:
+
+ 1. The client should check the `counterpartyInformation` against their requirements
+ 2. To APPROVE the payment synchronously, return a 200 OK response
+ 3. To REJECT the payment, return a 403 Forbidden response with an Error object
+ 4. To request more information, return a 422 Unprocessable Entity with specific missing fields
+ 5. To process the payment asynchronously, return a 202 Accepted response and then call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint within 5 seconds. Note that synchronous approval/rejection is preferred where possible.
+
+ The Grid system will proceed or cancel the payment based on your response.
+
+ For transactions with other statuses (COMPLETED, FAILED, REFUNDED), this webhook is purely informational.
+ operationId: incomingPaymentWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IncomingPaymentWebhook'
+ examples:
+ pendingPayment:
+ summary: Pending payment example requiring approval
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000007
+ type: INCOMING_PAYMENT.PENDING
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ status: PENDING
+ type: INCOMING
+ direction: CREDIT
+ destination:
+ destinationType: UMA_ADDRESS
+ umaAddress: $recipient@uma.domain
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ senderUmaAddress: $sender@external.domain
+ receiverUmaAddress: $recipient@uma.domain
+ receivedAmount:
+ amount: 50000
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ counterpartyInformation:
+ FULL_NAME: John Sender
+ BIRTH_DATE: '1985-06-15'
+ NATIONALITY: US
+ reconciliationInstructions:
+ reference: REF-123456789
+ requestedReceiverCustomerInfoFields:
+ - name: NATIONALITY
+ mandatory: true
+ - name: POSTAL_ADDRESS
+ mandatory: false
+ incomingCompletedPayment:
+ summary: Completed payment notification
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000007
+ type: INCOMING_PAYMENT.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ status: COMPLETED
+ type: INCOMING
+ direction: CREDIT
+ destination:
+ destinationType: UMA_ADDRESS
+ umaAddress: $recipient@uma.domain
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ senderUmaAddress: $sender@external.domain
+ receiverUmaAddress: $recipient@uma.domain
+ receivedAmount:
+ amount: 50000
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ settledAt: '2025-08-15T14:30:00Z'
+ createdAt: '2025-08-15T14:25:18Z'
+ description: Payment for services
+ reconciliationInstructions:
+ reference: REF-123456789
+ incomingCompletedCryptoPayment:
+ summary: Completed payment funded from an external crypto wallet
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000009
+ type: INCOMING_PAYMENT.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000006
+ status: COMPLETED
+ type: INCOMING
+ direction: CREDIT
+ source:
+ sourceType: REALTIME_FUNDING
+ currency: USDC
+ onChainTransaction:
+ transactionHash: 7RJWhvQBQPEjJmki5fhBboGBWRJhmcFkMvrr4Fu3tMSJ5EdynMEiYSyiWAH9GpcbHpeUzeSQF9ZY6q4x8AhBskUf
+ network: SOLANA
+ destination:
+ destinationType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ receivedAmount:
+ amount: 100000
+ currency:
+ code: USDC
+ name: USD Coin
+ symbol: ''
+ decimals: 6
+ settledAt: '2025-08-15T14:30:00Z'
+ createdAt: '2025-08-15T14:25:18Z'
+ description: USDC deposit from self-custody wallet
+ responses:
+ '200':
+ description: |
+ Webhook received successfully.
+ For PENDING transactions, this indicates approval to proceed with the payment.
+ If `requestedReceiverCustomerInfoFields` were present in the webhook request, the corresponding fields for the recipient must be included in this response in the `receiverCustomerInfo` object.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IncomingPaymentWebhookResponse'
+ '202':
+ description: |
+ Webhook received and will be processed asynchronously. The synchronous 200 response should be preferred where possible. This asycnhronous path should only be used in
+ cases where the platform's architecture requires async (but still very quick) processing before approving or rejecting the payment.
+ The platform must call the `/transactions/{transactionId}/approve` or `/transactions/{transactionId}/reject` endpoint to approve or reject the payment within 5 seconds or the payment will be automatically rejected.
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '403':
+ description: |
+ Forbidden - Payment rejected by the client.
+ Only applicable for PENDING transactions.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IncomingPaymentWebhookForbiddenResponse'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ '422':
+ description: |
+ Unprocessable Entity - Additional counterparty information required.
+ Only applicable for PENDING transactions.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/IncomingPaymentWebhookUnprocessableResponse'
+ outgoing-payment:
+ post:
+ summary: Outgoing payment status webhook
+ description: |
+ Webhook that is called when an outgoing payment's status changes.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ This webhook is informational only and is sent when an outgoing payment completes successfully, fails, or is refunded.
+ operationId: outgoingPaymentWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OutgoingPaymentWebhook'
+ examples:
+ outgoingCompletedPayment:
+ summary: Completed outgoing payment
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000007
+ type: OUTGOING_PAYMENT.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ status: COMPLETED
+ type: OUTGOING
+ direction: DEBIT
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ senderUmaAddress: $sender@uma.domain
+ receiverUmaAddress: $recipient@external.domain
+ sentAmount:
+ amount: 10550
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ receivedAmount:
+ amount: 9706
+ currency:
+ code: EUR
+ name: Euro
+ symbol: €
+ decimals: 2
+ exchangeRate: 0.92
+ quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006
+ settledAt: '2025-08-15T14:30:00Z'
+ createdAt: '2025-08-15T14:25:18Z'
+ description: 'Payment for invoice #1234'
+ paymentInstructions: []
+ rateDetails:
+ counterpartyMultiplier: 1.08
+ counterpartyFixedFee: 10
+ gridApiMultiplier: 0.925
+ gridApiFixedFee: 10
+ gridApiVariableFeeRate: 0.003
+ gridApiVariableFeeAmount: 30
+ outgoingCompletedCryptoPayment:
+ summary: Completed crypto payout to an external wallet
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000008
+ type: OUTGOING_PAYMENT.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000009
+ status: COMPLETED
+ type: OUTGOING
+ direction: DEBIT
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:c34dcbd6-dced-4ec4-b756-3c3a9ea3d789
+ onChainTransaction:
+ transactionHash: h82pJGF9p7kpzb6eU326EFZf2cDnimbTFVeJtx1qtBmUNJAEqN76R7PwPfHt3oWb8R6cKvhgyxQdDn53jFrK6wFx
+ network: SOLANA
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ sentAmount:
+ amount: 100000
+ currency:
+ code: USDC
+ name: USD Coin
+ symbol: ''
+ decimals: 6
+ receivedAmount:
+ amount: 100000
+ currency:
+ code: USDC
+ name: USD Coin
+ symbol: ''
+ decimals: 6
+ quoteId: Quote:019542f5-b3e7-1d02-0000-000000000010
+ settledAt: '2025-08-15T14:30:00Z'
+ createdAt: '2025-08-15T14:25:18Z'
+ description: USDC withdrawal to self-custody wallet
+ paymentInstructions: []
+ failedPayment:
+ summary: Failed outgoing payment
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000007
+ type: OUTGOING_PAYMENT.FAILED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Transaction:019542f5-b3e7-1d02-0000-000000000005
+ status: FAILED
+ type: OUTGOING
+ direction: DEBIT
+ source:
+ sourceType: ACCOUNT
+ accountId: InternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965
+ destination:
+ destinationType: ACCOUNT
+ accountId: ExternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123
+ customerId: Customer:019542f5-b3e7-1d02-0000-000000000001
+ platformCustomerId: 18d3e5f7b4a9c2
+ senderUmaAddress: $sender@uma.domain
+ receiverUmaAddress: $recipient@external.domain
+ sentAmount:
+ amount: 10550
+ currency:
+ code: USD
+ name: United States Dollar
+ symbol: $
+ decimals: 2
+ createdAt: '2025-08-15T14:25:18Z'
+ quoteId: Quote:019542f5-b3e7-1d02-0000-000000000006
+ failureReason: QUOTE_EXECUTION_FAILED
+ responses:
+ '200':
+ description: Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ test-webhook:
+ post:
+ summary: Test webhook for integration verification
+ description: |
+ Webhook that is sent once to verify your webhook endpoint is correctly set up.
+ This is sent when you configure or update your platform settings with a webhook URL.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by the Grid API.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ This webhook is purely for testing your endpoint integration and signature verification.
+ operationId: testWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TestWebhookRequest'
+ examples:
+ testWebhook:
+ summary: Test webhook example
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000001
+ type: TEST
+ timestamp: '2025-08-15T14:32:00Z'
+ data: {}
+ responses:
+ '200':
+ description: Webhook received successfully. This confirms your webhook endpoint is properly configured.
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ bulk-upload:
+ post:
+ summary: Bulk upload status webhook
+ description: |
+ Webhook that is called when a bulk customer upload job completes or fails.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+
+ This webhook is sent when a bulk upload job completes or fails, providing detailed information about the results.
+ operationId: bulkUploadWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BulkUploadWebhook'
+ examples:
+ completedUpload:
+ summary: Successful bulk upload completion
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000008
+ type: BULK_UPLOAD.COMPLETED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Job:019542f5-b3e7-1d02-0000-000000000006
+ status: COMPLETED
+ progress:
+ total: 5000
+ processed: 5000
+ successful: 5000
+ failed: 0
+ errors: []
+ failedUpload:
+ summary: Failed bulk upload
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000008
+ type: BULK_UPLOAD.FAILED
+ timestamp: '2025-08-15T14:32:00Z'
+ data:
+ id: Job:019542f5-b3e7-1d02-0000-000000000006
+ status: FAILED
+ progress:
+ total: 5000
+ processed: 5000
+ successful: 0
+ failed: 5000
+ errors:
+ - correlationId: row_1
+ error:
+ code: invalid_csv_format
+ message: Invalid CSV format
+ details:
+ reason: missing_required_column
+ column: umaAddress
+ responses:
+ '200':
+ description: Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ invitation-claimed:
+ post:
+ summary: Invitation claimed webhook
+ description: |
+ Webhook that is called when an invitation is claimed by a customer.
+ This endpoint should be implemented by platform clients of the Grid API.
+
+ When a customer claims an invitation, this webhook is triggered to notify the platform that:
+ 1. The invitation has been successfully claimed
+ 2. The invitee UMA address is now associated with the invitation
+ 3. The invitation status has changed from PENDING to CLAIMED
+
+ This allows platforms to:
+ - Track invitation usage and conversion rates
+ - Trigger onboarding flows for new customers who joined via invitation
+ - Apply referral bonuses or rewards to the inviter
+ - Update their UI to reflect the claimed status
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+ operationId: invitationClaimedWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/InvitationClaimedWebhook'
+ examples:
+ claimedInvitation:
+ summary: Invitation claimed notification
+ value:
+ id: Webhook:019542f5-b3e7-1d02-0000-000000000008
+ type: INVITATION.CLAIMED
+ timestamp: '2025-09-01T15:45:00Z'
+ data:
+ code: 019542f5
+ createdAt: '2025-09-01T14:30:00Z'
+ claimedAt: '2025-09-01T15:45:00Z'
+ inviterUma: $inviter@uma.domain
+ inviteeUma: $invitee@uma.domain
+ status: CLAIMED
+ url: https://uma.me/i/019542f5
+ responses:
+ '200':
+ description: Webhook received successfully
+ '400':
+ description: Bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error400'
+ '401':
+ description: Unauthorized - Signature validation failed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error401'
+ '409':
+ description: Conflict - Webhook has already been processed (duplicate id)
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error409'
+ customer-update:
+ post:
+ summary: Customer status change
+ description: |
+ Webhook that is called when the status of a customer is updated, including KYC and KYB status changes.
+ This endpoint should be implemented by clients of the Grid API.
+
+ ### Authentication
+ The webhook includes a signature in the `X-Grid-Signature` header that allows you to verify that the webhook was sent by Grid.
+ To verify the signature:
+ 1. Get the Grid API public key provided to you during integration
+ 2. Decode the base64 signature from the header
+ 3. Create a SHA-256 hash of the request body
+ 4. Verify the signature using the public key and the hash
+
+ If the signature verification succeeds, the webhook is authentic. If not, it should be rejected.
+ operationId: customerStatusWebhook
+ tags:
+ - Webhooks
+ security:
+ - WebhookSignature: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CustomerWebhook'
+ examples:
+ kycApprovedWebhook:
+ summary: When an individual customer KYC has been approved
value:
id: Webhook:019542f5-b3e7-1d02-0000-000000000007
type: CUSTOMER.KYC_APPROVED
@@ -10110,7 +11018,131 @@ components:
example: OPERATING_REVENUE
BusinessInfoUpdate:
type: object
- description: Additional information for business entities
+ description: Additional information for business entities
+ properties:
+ legalName:
+ type: string
+ description: Legal name of the business
+ example: Acme Corporation, Inc.
+ doingBusinessAs:
+ type: string
+ description: Trade name or DBA name of the business, if different from the legal name
+ example: Acme
+ country:
+ type: string
+ description: Country of incorporation or registration (ISO 3166-1 alpha-2)
+ example: US
+ registrationNumber:
+ type: string
+ description: Business registration number
+ example: '5523041'
+ incorporatedOn:
+ type: string
+ format: date
+ description: Date of incorporation in ISO 8601 format (YYYY-MM-DD)
+ example: '2018-03-14'
+ entityType:
+ $ref: '#/components/schemas/EntityType'
+ taxId:
+ type: string
+ description: Tax identification number
+ example: 47-1234567
+ countriesOfOperation:
+ type: array
+ items:
+ type: string
+ description: List of countries where the business operates (ISO 3166-1 alpha-2)
+ example:
+ - US
+ businessType:
+ $ref: '#/components/schemas/BusinessType'
+ purposeOfAccount:
+ $ref: '#/components/schemas/PurposeOfAccount'
+ sourceOfFunds:
+ type: string
+ description: The primary source of funds for the business
+ example: Funds derived from customer payments for software services
+ expectedMonthlyTransactionCount:
+ type: string
+ enum:
+ - COUNT_UNDER_10
+ - COUNT_10_TO_100
+ - COUNT_100_TO_500
+ - COUNT_500_TO_1000
+ - COUNT_OVER_1000
+ description: Expected number of transactions per month
+ example: COUNT_100_TO_500
+ expectedMonthlyTransactionVolume:
+ type: string
+ enum:
+ - VOLUME_UNDER_10K
+ - VOLUME_10K_TO_100K
+ - VOLUME_100K_TO_1M
+ - VOLUME_1M_TO_10M
+ - VOLUME_OVER_10M
+ description: Expected total transaction volume per month in USD equivalent
+ example: VOLUME_100K_TO_1M
+ expectedRecipientJurisdictions:
+ type: array
+ items:
+ type: string
+ description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2)
+ example:
+ - US
+ naicsCode:
+ type: string
+ pattern: ^\d{2,6}$
+ description: NAICS code describing the nature of the business (2-6 digits)
+ example: '541511'
+ sourceOfFundsCategories:
+ type: array
+ items:
+ $ref: '#/components/schemas/SourceOfFundsCategory'
+ description: Structured source-of-funds categories for the business
+ example:
+ - OPERATING_REVENUE
+ sourceOfFundsOtherDescription:
+ type: string
+ minLength: 1
+ maxLength: 500
+ description: Description of the source of funds when OTHER is selected
+ example: Proceeds from a legal settlement
+ purposeOfAccountOtherDescription:
+ type: string
+ minLength: 1
+ maxLength: 500
+ description: Description of the account purpose when OTHER is selected
+ example: Escrow for equipment leases
+ expectedCounterpartyCountries:
+ type: array
+ items:
+ type: string
+ description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
+ example:
+ - US
+ BusinessCustomerFields:
+ type: object
+ required:
+ - customerType
+ properties:
+ customerType:
+ type: string
+ enum:
+ - BUSINESS
+ kybStatus:
+ $ref: '#/components/schemas/KybStatus'
+ address:
+ $ref: '#/components/schemas/Address'
+ businessInfo:
+ $ref: '#/components/schemas/BusinessInfoUpdate'
+ BusinessInfoResponse:
+ type: object
+ description: |
+ Business information returned on a customer. `taxId` and `incorporatedOn` are
+ required on creation but may be absent on legacy customers that pre-date the
+ requirement, so both are optional in responses.
+ required:
+ - legalName
properties:
legalName:
type: string
@@ -10157,84 +11189,309 @@ components:
expectedMonthlyTransactionCount:
type: string
enum:
- - COUNT_UNDER_10
- - COUNT_10_TO_100
- - COUNT_100_TO_500
- - COUNT_500_TO_1000
- - COUNT_OVER_1000
- description: Expected number of transactions per month
- example: COUNT_100_TO_500
- expectedMonthlyTransactionVolume:
+ - COUNT_UNDER_10
+ - COUNT_10_TO_100
+ - COUNT_100_TO_500
+ - COUNT_500_TO_1000
+ - COUNT_OVER_1000
+ description: Expected number of transactions per month
+ example: COUNT_100_TO_500
+ expectedMonthlyTransactionVolume:
+ type: string
+ enum:
+ - VOLUME_UNDER_10K
+ - VOLUME_10K_TO_100K
+ - VOLUME_100K_TO_1M
+ - VOLUME_1M_TO_10M
+ - VOLUME_OVER_10M
+ description: Expected total transaction volume per month in USD equivalent
+ example: VOLUME_100K_TO_1M
+ expectedRecipientJurisdictions:
+ type: array
+ items:
+ type: string
+ description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2)
+ example:
+ - US
+ naicsCode:
+ type: string
+ pattern: ^\d{2,6}$
+ description: NAICS code describing the nature of the business (2-6 digits)
+ example: '541511'
+ sourceOfFundsCategories:
+ type: array
+ items:
+ $ref: '#/components/schemas/SourceOfFundsCategory'
+ description: Structured source-of-funds categories for the business
+ example:
+ - OPERATING_REVENUE
+ sourceOfFundsOtherDescription:
+ type: string
+ minLength: 1
+ maxLength: 500
+ description: Description of the source of funds when OTHER is selected
+ example: Proceeds from a legal settlement
+ purposeOfAccountOtherDescription:
+ type: string
+ minLength: 1
+ maxLength: 500
+ description: Description of the account purpose when OTHER is selected
+ example: Escrow for equipment leases
+ expectedCounterpartyCountries:
+ type: array
+ items:
+ type: string
+ description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
+ example:
+ - US
+ BeneficialOwnerRole:
+ type: string
+ enum:
+ - UBO
+ - DIRECTOR
+ - COMPANY_OFFICER
+ - CONTROL_PERSON
+ - TRUSTEE
+ - GENERAL_PARTNER
+ description: Role of the beneficial owner within the business
+ example: UBO
+ IdentificationType:
+ type: string
+ enum:
+ - SSN
+ - ITIN
+ - EIN
+ - NON_US_TAX_ID
+ description: Type of tax identification
+ example: SSN
+ BeneficialOwnerPersonalInfo:
+ type: object
+ required:
+ - firstName
+ - lastName
+ - birthDate
+ - nationality
+ - address
+ - idType
+ - identifier
+ properties:
+ firstName:
+ type: string
+ description: First name of the individual
+ example: Jane
+ middleName:
+ type: string
+ description: Middle name of the individual
+ example: Marie
+ lastName:
+ type: string
+ description: Last name of the individual
+ example: Smith
+ birthDate:
+ type: string
+ format: date
+ description: Date of birth in ISO 8601 format (YYYY-MM-DD)
+ example: '1978-06-15'
+ nationality:
+ type: string
+ description: Country of nationality (ISO 3166-1 alpha-2)
+ example: US
+ email:
+ type: string
+ format: email
+ description: Email address of the individual
+ example: jane.smith@acmecorp.com
+ phoneNumber:
+ type: string
+ description: Phone number in E.164 format
+ example: '+14155550192'
+ pattern: ^\+[1-9]\d{1,14}$
+ address:
+ $ref: '#/components/schemas/Address'
+ idType:
+ $ref: '#/components/schemas/IdentificationType'
+ identifier:
+ type: string
+ description: The identification number or value
+ example: 123-45-6789
+ countryOfIssuance:
+ type: string
+ description: Country that issued the identification (ISO 3166-1 alpha-2)
+ example: US
+ BeneficialOwner:
+ type: object
+ required:
+ - id
+ - customerId
+ - roles
+ - ownershipPercentage
+ - personalInfo
+ - kycStatus
+ - createdAt
+ properties:
+ id:
+ type: string
+ description: Unique identifier for this beneficial owner
+ example: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000001
+ customerId:
+ type: string
+ description: The ID of the business customer this beneficial owner is associated with
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ roles:
+ type: array
+ items:
+ $ref: '#/components/schemas/BeneficialOwnerRole'
+ description: Roles of this person within the business
+ example:
+ - UBO
+ - DIRECTOR
+ ownershipPercentage:
+ type: integer
+ description: Percentage of ownership in the business (0-100)
+ minimum: 0
+ maximum: 100
+ example: 51
+ personalInfo:
+ $ref: '#/components/schemas/BeneficialOwnerPersonalInfo'
+ kycStatus:
+ $ref: '#/components/schemas/KycStatus'
+ createdAt:
+ type: string
+ format: date-time
+ description: When this beneficial owner was created
+ example: '2025-10-03T12:00:00Z'
+ updatedAt:
+ type: string
+ format: date-time
+ description: When this beneficial owner was last updated
+ example: '2025-10-03T12:00:00Z'
+ BusinessCustomer:
+ title: Business Customer
+ allOf:
+ - $ref: '#/components/schemas/Customer'
+ - $ref: '#/components/schemas/BusinessCustomerFields'
+ - type: object
+ properties:
+ businessInfo:
+ $ref: '#/components/schemas/BusinessInfoResponse'
+ beneficialOwners:
+ type: array
+ items:
+ $ref: '#/components/schemas/BeneficialOwner'
+ CustomerOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/IndividualCustomer'
+ - $ref: '#/components/schemas/BusinessCustomer'
+ discriminator:
+ propertyName: customerType
+ mapping:
+ INDIVIDUAL: '#/components/schemas/IndividualCustomer'
+ BUSINESS: '#/components/schemas/BusinessCustomer'
+ CustomerListResponse:
+ type: object
+ required:
+ - data
+ - hasMore
+ properties:
+ data:
+ type: array
+ description: List of customers matching the filter criteria
+ items:
+ $ref: '#/components/schemas/CustomerOneOf'
+ hasMore:
+ type: boolean
+ description: Indicates if more results are available beyond this page
+ nextCursor:
+ type: string
+ description: Cursor to retrieve the next page of results (only present if hasMore is true)
+ totalCount:
+ type: integer
+ description: Total number of customers matching the criteria (excluding pagination)
+ Error405:
+ type: object
+ required:
+ - message
+ - status
+ - code
+ properties:
+ status:
+ type: integer
+ enum:
+ - 405
+ description: HTTP status code
+ code:
type: string
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | METHOD_NOT_ALLOWED | The HTTP method is not supported for this endpoint |
enum:
- - VOLUME_UNDER_10K
- - VOLUME_10K_TO_100K
- - VOLUME_100K_TO_1M
- - VOLUME_1M_TO_10M
- - VOLUME_OVER_10M
- description: Expected total transaction volume per month in USD equivalent
- example: VOLUME_100K_TO_1M
- expectedRecipientJurisdictions:
- type: array
- items:
- type: string
- description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2)
- example:
- - US
- naicsCode:
+ - METHOD_NOT_ALLOWED
+ message:
type: string
- pattern: ^\d{2,6}$
- description: NAICS code describing the nature of the business (2-6 digits)
- example: '541511'
- sourceOfFundsCategories:
- type: array
- items:
- $ref: '#/components/schemas/SourceOfFundsCategory'
- description: Structured source-of-funds categories for the business
- example:
- - OPERATING_REVENUE
- sourceOfFundsOtherDescription:
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ CustomerCreateRequest:
+ type: object
+ required:
+ - customerType
+ properties:
+ platformCustomerId:
type: string
- minLength: 1
- maxLength: 500
- description: Description of the source of funds when OTHER is selected
- example: Proceeds from a legal settlement
- purposeOfAccountOtherDescription:
+ description: Platform-specific customer identifier. If not provided, one will be generated by the system.
+ example: 9f84e0c2a72c4fa
+ customerType:
+ $ref: '#/components/schemas/CustomerType'
+ region:
type: string
- minLength: 1
- maxLength: 500
- description: Description of the account purpose when OTHER is selected
- example: Escrow for equipment leases
- expectedCounterpartyCountries:
+ description: Country code (ISO 3166-1 alpha-2) representing the customer's regional identity. This determines the regulatory jurisdiction and KYC requirements for the customer. Required if the customer will use currencies with different KYC requirements across regions. A customer with accounts in multiple regions should be registered as separate customers. This field is immutable after creation.
+ example: US
+ currencies:
type: array
items:
type: string
- description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
+ description: List of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Required if the customer will use more than one sending currency, since the correct currencies cannot always be inferred. If not provided, currencies will be inferred from the customer's region. Some currency combinations may require separate customers — if so, the request will be rejected with details.
example:
- - US
- BusinessCustomerFields:
- type: object
- required:
- - customerType
- properties:
- customerType:
+ - USD
+ - USDC
+ email:
type: string
- enum:
- - BUSINESS
- kybStatus:
- $ref: '#/components/schemas/KybStatus'
- address:
- $ref: '#/components/schemas/Address'
- businessInfo:
- $ref: '#/components/schemas/BusinessInfoUpdate'
- BusinessInfoResponse:
+ format: email
+ description: Email address for the customer.
+ example: john.doe@example.com
+ phoneNumber:
+ type: string
+ pattern: ^\+[1-9]\d{1,14}$
+ description: Phone number for the customer in strict E.164 format.
+ example: '+14155551234'
+ umaAddress:
+ type: string
+ description: Optional UMA address identifier. If not provided during customer creation, one will be generated by the system. If provided during customer update, the UMA address will be updated to the provided value. This is an optional identifier to route payments to the customer. This is an optional identifier to route payments to the customer.
+ example: $john.doe@uma.domain.com
+ IndividualCustomerCreateRequest:
+ title: Individual Customer Create Request
+ allOf:
+ - $ref: '#/components/schemas/CustomerCreateRequest'
+ - $ref: '#/components/schemas/IndividualCustomerFields'
+ - type: object
+ properties:
+ idType:
+ $ref: '#/components/schemas/IdentificationType'
+ identifier:
+ type: string
+ writeOnly: true
+ description: The individual's tax identification number. Required to onboard the individual as a US account holder. Only SSN and ITIN are currently accepted for an individual account holder; other identification types are rejected. Write-only — never returned in customer responses.
+ example: 123-45-6789
+ BusinessInfo:
type: object
- description: |
- Business information returned on a customer. `taxId` and `incorporatedOn` are
- required on creation but may be absent on legacy customers that pre-date the
- requirement, so both are optional in responses.
+ description: Additional information required for business entities
required:
- legalName
+ - taxId
+ - incorporatedOn
properties:
legalName:
type: string
@@ -10336,454 +11593,621 @@ components:
description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
example:
- US
- BeneficialOwnerRole:
- type: string
- enum:
- - UBO
- - DIRECTOR
- - COMPANY_OFFICER
- - CONTROL_PERSON
- - TRUSTEE
- - GENERAL_PARTNER
- description: Role of the beneficial owner within the business
- example: UBO
- IdentificationType:
- type: string
- enum:
- - SSN
- - ITIN
- - EIN
- - NON_US_TAX_ID
- description: Type of tax identification
- example: SSN
- BeneficialOwnerPersonalInfo:
+ BusinessCustomerCreateRequest:
+ title: Business Customer Create Request
+ allOf:
+ - $ref: '#/components/schemas/CustomerCreateRequest'
+ - $ref: '#/components/schemas/BusinessCustomerFields'
+ - type: object
+ properties:
+ businessInfo:
+ $ref: '#/components/schemas/BusinessInfo'
+ CustomerCreateRequestOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/IndividualCustomerCreateRequest'
+ - $ref: '#/components/schemas/BusinessCustomerCreateRequest'
+ discriminator:
+ propertyName: customerType
+ mapping:
+ INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest'
+ BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest'
+ Error409:
+ type: object
+ required:
+ - message
+ - status
+ - code
+ properties:
+ status:
+ type: integer
+ enum:
+ - 409
+ description: HTTP status code
+ code:
+ type: string
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL | Transaction is not pending platform approval |
+ | UMA_ADDRESS_EXISTS | UMA address already exists |
+ | EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential |
+ | EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued |
+ | PASSKEY_ALREADY_ENROLLED | The customer already has an enrolled passkey factor; only one passkey per customer is supported. Delete the existing one before enrolling another |
+ | CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform |
+ enum:
+ - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL
+ - UMA_ADDRESS_EXISTS
+ - EMAIL_OTP_EMAIL_ALREADY_EXISTS
+ - EMAIL_OTP_CREDENTIAL_SET_CHANGED
+ - PASSKEY_ALREADY_ENROLLED
+ - CONFLICT
+ message:
+ type: string
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ Error404:
+ type: object
+ required:
+ - message
+ - status
+ - code
+ properties:
+ status:
+ type: integer
+ enum:
+ - 404
+ description: HTTP status code
+ code:
+ type: string
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | TRANSACTION_NOT_FOUND | Transaction not found |
+ | INVITATION_NOT_FOUND | Invitation not found |
+ | USER_NOT_FOUND | Customer not found |
+ | QUOTE_NOT_FOUND | Quote not found |
+ | LOOKUP_REQUEST_NOT_FOUND | Lookup request not found |
+ | TOKEN_NOT_FOUND | Token not found |
+ | BULK_UPLOAD_JOB_NOT_FOUND | Bulk upload job not found |
+ | REFERENCE_NOT_FOUND | Reference not found |
+ | UMA_NOT_FOUND | The UMA address is well-formed but no receiver exists at the counterparty VASP |
+ | STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND | Stablecoin provider account link not found |
+ enum:
+ - TRANSACTION_NOT_FOUND
+ - INVITATION_NOT_FOUND
+ - USER_NOT_FOUND
+ - QUOTE_NOT_FOUND
+ - LOOKUP_REQUEST_NOT_FOUND
+ - TOKEN_NOT_FOUND
+ - BULK_UPLOAD_JOB_NOT_FOUND
+ - REFERENCE_NOT_FOUND
+ - UMA_NOT_FOUND
+ - STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND
+ message:
+ type: string
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ Error410:
type: object
required:
- - firstName
- - lastName
- - birthDate
- - nationality
- - address
- - idType
- - identifier
+ - message
+ - status
+ - code
properties:
- firstName:
- type: string
- description: First name of the individual
- example: Jane
- middleName:
- type: string
- description: Middle name of the individual
- example: Marie
- lastName:
- type: string
- description: Last name of the individual
- example: Smith
- birthDate:
+ status:
+ type: integer
+ enum:
+ - 410
+ description: HTTP status code
+ code:
type: string
- format: date
- description: Date of birth in ISO 8601 format (YYYY-MM-DD)
- example: '1978-06-15'
- nationality:
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | CUSTOMER_DELETED | Customer has been permanently deleted |
+ enum:
+ - CUSTOMER_DELETED
+ message:
type: string
- description: Country of nationality (ISO 3166-1 alpha-2)
- example: US
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ CustomerUpdateRequest:
+ title: Customer Update Request
+ description: Request body for `PATCH /customers/{customerId}`. When `email` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer email and every tied `EMAIL_OTP` credential through the endpoint's signed-retry flow. When `phoneNumber` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer phone number and every tied `SMS_OTP` credential through the same signed-retry flow. Update `email` and `phoneNumber` in separate PATCH calls.
+ type: object
+ required:
+ - customerType
+ properties:
+ customerType:
+ $ref: '#/components/schemas/CustomerType'
+ currencies:
+ type: array
+ items:
+ type: string
+ description: Updated list of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Replaces the existing list. Some currency combinations may require separate customers — if so, the request will be rejected with details.
+ example:
+ - USD
+ - EUR
+ - USDC
email:
type: string
format: email
- description: Email address of the individual
- example: jane.smith@acmecorp.com
+ description: Email address for the customer. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `EMAIL_OTP` credential across all tied Embedded Wallets.
+ example: john.doe@example.com
phoneNumber:
type: string
- description: Phone number in E.164 format
- example: '+14155550192'
pattern: ^\+[1-9]\d{1,14}$
- address:
- $ref: '#/components/schemas/Address'
- idType:
- $ref: '#/components/schemas/IdentificationType'
- identifier:
- type: string
- description: The identification number or value
- example: 123-45-6789
- countryOfIssuance:
+ description: Phone number for the customer in strict E.164 format. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `SMS_OTP` credential across all tied Embedded Wallets. Send phone number and email updates as separate PATCH calls.
+ example: '+14155551234'
+ umaAddress:
type: string
- description: Country that issued the identification (ISO 3166-1 alpha-2)
- example: US
- BeneficialOwner:
+ description: Optional UMA address identifier. If provided, the customer's UMA address will be updated. This is an optional identifier to route payments to the customer.
+ example: $john.doe@uma.domain.com
+ IndividualCustomerUpdateRequest:
+ title: Individual Customer Update Request
+ allOf:
+ - $ref: '#/components/schemas/CustomerUpdateRequest'
+ - $ref: '#/components/schemas/IndividualCustomerFields'
+ BusinessCustomerUpdateRequest:
+ title: Business Customer Update Request
+ allOf:
+ - $ref: '#/components/schemas/CustomerUpdateRequest'
+ - $ref: '#/components/schemas/BusinessCustomerFields'
+ CustomerUpdateRequestOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/IndividualCustomerUpdateRequest'
+ - $ref: '#/components/schemas/BusinessCustomerUpdateRequest'
+ discriminator:
+ propertyName: customerType
+ mapping:
+ INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest'
+ BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest'
+ SignedRequestChallenge:
+ title: Signed Request Challenge
type: object
required:
- - id
- - customerId
- - roles
- - ownershipPercentage
- - personalInfo
- - kycStatus
- - createdAt
+ - payloadToSign
+ - requestId
+ - expiresAt
+ description: Common base for two-step signed-retry challenge responses on Embedded Wallet endpoints (credential registration or revocation, session refresh or revocation, wallet export, customer email updates, and similar). Holds the signing fields shared across every challenge shape; each variant composes this base via `allOf` and adds its own resource `id` (and `type`, when applicable) with variant-specific description and example.
properties:
- id:
+ payloadToSign:
type: string
- description: Unique identifier for this beneficial owner
- example: BeneficialOwner:019542f5-b3e7-1d02-0000-000000000001
- customerId:
+ description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request.
+ example: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_EXAMPLE"}'
+ requestId:
type: string
- description: The ID of the business customer this beneficial owner is associated with
- example: Customer:019542f5-b3e7-1d02-0000-000000000001
- roles:
- type: array
- items:
- $ref: '#/components/schemas/BeneficialOwnerRole'
- description: Roles of this person within the business
- example:
- - UBO
- - DIRECTOR
- ownershipPercentage:
- type: integer
- description: Percentage of ownership in the business (0-100)
- minimum: 0
- maximum: 100
- example: 51
- personalInfo:
- $ref: '#/components/schemas/BeneficialOwnerPersonalInfo'
- kycStatus:
- $ref: '#/components/schemas/KycStatus'
- createdAt:
+ description: Grid-issued `Request:` identifier for this pending request. Echo this value exactly in the `Request-Id` header on the signed retry so the server can correlate the retry with the issued challenge.
+ example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ expiresAt:
type: string
format: date-time
- description: When this beneficial owner was created
- example: '2025-10-03T12:00:00Z'
- updatedAt:
+ description: Timestamp after which this challenge is no longer valid. The signed retry must be submitted before this time.
+ example: '2026-04-08T15:35:00Z'
+ Error424:
+ type: object
+ required:
+ - message
+ - status
+ - code
+ properties:
+ status:
+ type: integer
+ enum:
+ - 424
+ description: HTTP status code
+ code:
type: string
- format: date-time
- description: When this beneficial owner was last updated
- example: '2025-10-03T12:00:00Z'
- BusinessCustomer:
- title: Business Customer
- allOf:
- - $ref: '#/components/schemas/Customer'
- - $ref: '#/components/schemas/BusinessCustomerFields'
- - type: object
- properties:
- businessInfo:
- $ref: '#/components/schemas/BusinessInfoResponse'
- beneficialOwners:
- type: array
- items:
- $ref: '#/components/schemas/BeneficialOwner'
- CustomerOneOf:
- oneOf:
- - $ref: '#/components/schemas/IndividualCustomer'
- - $ref: '#/components/schemas/BusinessCustomer'
- discriminator:
- propertyName: customerType
- mapping:
- INDIVIDUAL: '#/components/schemas/IndividualCustomer'
- BUSINESS: '#/components/schemas/BusinessCustomer'
- CustomerListResponse:
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | PAYREQ_REQUEST_FAILED | Payment request failed |
+ | COUNTERPARTY_PUBKEY_FETCH_ERROR | Error fetching counterparty public key |
+ | NO_COMPATIBLE_UMA_VERSION | No compatible UMA version |
+ | LNURLP_REQUEST_FAILED | LNURLP request failed |
+ | EMAIL_OTP_CREDENTIAL_SYNC_FAILED | Failed to update one or more tied EMAIL_OTP credentials |
+ enum:
+ - PAYREQ_REQUEST_FAILED
+ - COUNTERPARTY_PUBKEY_FETCH_ERROR
+ - NO_COMPATIBLE_UMA_VERSION
+ - LNURLP_REQUEST_FAILED
+ - EMAIL_OTP_CREDENTIAL_SYNC_FAILED
+ message:
+ type: string
+ description: Error message
+ details:
+ type: object
+ description: Additional error details
+ additionalProperties: true
+ KycLinkCreateRequest:
+ type: object
+ description: Request body for generating a hosted KYC link for an existing customer.
+ properties:
+ redirectUri:
+ type: string
+ format: uri
+ description: URI the customer is redirected to after completing the hosted KYC flow. Must start with `https://` (or `http://` for local development). Embedded in the returned `kycUrl`.
+ example: https://app.example.com/onboarding/completed
+ KycProvider:
+ type: string
+ description: The KYC provider that will perform identity verification for the customer. Grid selects the provider based on the customer's region and platform configuration; the value is informational for platforms that want to integrate directly with the provider's SDK.
+ enum:
+ - SUMSUB
+ example: SUMSUB
+ KycLinkResponse:
type: object
+ description: A hosted KYC link that the customer can complete to verify their identity.
required:
- - data
- - hasMore
+ - kycUrl
+ - expiresAt
+ - provider
properties:
- data:
- type: array
- description: List of customers matching the filter criteria
- items:
- $ref: '#/components/schemas/CustomerOneOf'
- hasMore:
- type: boolean
- description: Indicates if more results are available beyond this page
- nextCursor:
+ kycUrl:
type: string
- description: Cursor to retrieve the next page of results (only present if hasMore is true)
- totalCount:
- type: integer
- description: Total number of customers matching the criteria (excluding pagination)
- Error405:
+ description: Hosted URL the customer should be sent to in order to complete verification. The URL is single-use and expires at `expiresAt`. To generate a new link (for example, after the previous one expires or is abandoned), call this endpoint again.
+ example: https://kyc.lightspark.com/onboard/abc123def456
+ expiresAt:
+ type: string
+ format: date-time
+ description: Time at which the hosted link expires and can no longer be used.
+ example: '2027-01-15T14:32:00Z'
+ provider:
+ $ref: '#/components/schemas/KycProvider'
+ token:
+ type: string
+ description: Provider-specific token that can be used in place of the hosted URL — for example, to embed the provider's SDK directly in your application. Only returned for providers that support direct SDK integration. Whether to use the hosted URL or the embedded SDK is up to you; both flows result in the same `kycStatus` update on the customer.
+ example: _act-sbx-jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+ ContactVerificationConfirmRequest:
type: object
+ description: Request body for confirming an email or phone verification challenge.
required:
- - message
- - status
- code
properties:
- status:
- type: integer
- enum:
- - 405
- description: HTTP status code
code:
type: string
- description: |
- | Error Code | Description |
- |------------|-------------|
- | METHOD_NOT_ALLOWED | The HTTP method is not supported for this endpoint |
- enum:
- - METHOD_NOT_ALLOWED
- message:
- type: string
- description: Error message
- details:
- type: object
- description: Additional error details
- additionalProperties: true
- CustomerCreateRequest:
+ description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`.
+ example: '123456'
+ ScaFactor:
+ type: string
+ enum:
+ - SMS_OTP
+ - TOTP
+ - PASSKEY
+ description: |
+ A Strong Customer Authentication factor.
+
+ | Factor | Description |
+ |--------|-------------|
+ | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. |
+ | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). |
+ | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. |
+ ScaFactorView:
type: object
+ description: An enrolled Strong Customer Authentication factor.
required:
- - customerType
+ - factor
properties:
- platformCustomerId:
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The kind of enrolled factor.
+ credentialId:
type: string
- description: Platform-specific customer identifier. If not provided, one will be generated by the system.
- example: 9f84e0c2a72c4fa
- customerType:
- $ref: '#/components/schemas/CustomerType'
- region:
+ description: The per-credential id, populated only for `PASSKEY` factors (the id passed to delete a passkey). Omitted for `TOTP` and `SMS_OTP`, which have no per-credential id.
+ name:
type: string
- description: Country code (ISO 3166-1 alpha-2) representing the customer's regional identity. This determines the regulatory jurisdiction and KYC requirements for the customer. Required if the customer will use currencies with different KYC requirements across regions. A customer with accounts in multiple regions should be registered as separate customers. This field is immutable after creation.
- example: US
- currencies:
+ description: An optional human-readable label for this factor.
+ ScaFactorList:
+ type: object
+ description: The Strong Customer Authentication factors a customer has enrolled.
+ required:
+ - factors
+ properties:
+ factors:
type: array
+ description: The customer's enrolled SCA factors.
items:
- type: string
- description: List of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Required if the customer will use more than one sending currency, since the correct currencies cannot always be inferred. If not provided, currencies will be inferred from the customer's region. Some currency combinations may require separate customers — if so, the request will be rejected with details.
- example:
- - USD
- - USDC
- email:
- type: string
- format: email
- description: Email address for the customer.
- example: john.doe@example.com
- phoneNumber:
- type: string
- pattern: ^\+[1-9]\d{1,14}$
- description: Phone number for the customer in strict E.164 format.
- example: '+14155551234'
- umaAddress:
+ $ref: '#/components/schemas/ScaFactorView'
+ TotpFactorEnrollRequest:
+ type: object
+ title: TOTP Factor Enroll Request
+ description: Start enrolling a time-based one-time-password (TOTP) authenticator factor.
+ required:
+ - type
+ properties:
+ type:
type: string
- description: Optional UMA address identifier. If not provided during customer creation, one will be generated by the system. If provided during customer update, the UMA address will be updated to the provided value. This is an optional identifier to route payments to the customer. This is an optional identifier to route payments to the customer.
- example: $john.doe@uma.domain.com
- IndividualCustomerCreateRequest:
- title: Individual Customer Create Request
- allOf:
- - $ref: '#/components/schemas/CustomerCreateRequest'
- - $ref: '#/components/schemas/IndividualCustomerFields'
- - type: object
- properties:
- idType:
- $ref: '#/components/schemas/IdentificationType'
- identifier:
- type: string
- writeOnly: true
- description: The individual's tax identification number. Required to onboard the individual as a US account holder. Only SSN and ITIN are currently accepted for an individual account holder; other identification types are rejected. Write-only — never returned in customer responses.
- example: 123-45-6789
- BusinessInfo:
+ enum:
+ - TOTP
+ description: Discriminator selecting the TOTP factor. TOTP enrollment needs no other input at start.
+ PasskeyFactorEnrollRequest:
type: object
- description: Additional information required for business entities
+ title: Passkey Factor Enroll Request
+ description: Start enrolling a WebAuthn passkey factor.
required:
- - legalName
- - taxId
- - incorporatedOn
+ - type
properties:
- legalName:
+ type:
type: string
- description: Legal name of the business
- example: Acme Corporation, Inc.
- doingBusinessAs:
+ enum:
+ - PASSKEY
+ description: Discriminator selecting the passkey factor. Passkey enrollment needs no other input at start.
+ ScaFactorEnrollRequestOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/TotpFactorEnrollRequest'
+ - $ref: '#/components/schemas/PasskeyFactorEnrollRequest'
+ discriminator:
+ propertyName: type
+ mapping:
+ TOTP: '#/components/schemas/TotpFactorEnrollRequest'
+ PASSKEY: '#/components/schemas/PasskeyFactorEnrollRequest'
+ description: Which SCA factor to begin enrolling, selected by `type`. `SMS_OTP` is not enrollable (it uses the customer's verified phone), so only `TOTP` and `PASSKEY` are valid here.
+ TotpEnrollmentStart:
+ type: object
+ description: The shared secret a customer's authenticator app needs to enroll a TOTP factor. Returned by `POST /sca/factors` for a `TOTP` request; the customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with the first code their app produces.
+ required:
+ - type
+ - secret
+ - secretBase32Encoded
+ - totpUri
+ properties:
+ type:
type: string
- description: Trade name or DBA name of the business, if different from the legal name
- example: Acme
- country:
+ enum:
+ - TOTP
+ description: Discriminator identifying this as the TOTP enrollment-start payload.
+ secret:
type: string
- description: Country of incorporation or registration (ISO 3166-1 alpha-2)
- example: US
- registrationNumber:
+ description: The raw TOTP shared secret.
+ secretBase32Encoded:
type: string
- description: Business registration number
- example: '5523041'
- incorporatedOn:
+ description: The Base32-encoded shared secret, suitable for manual entry into an authenticator app that does not scan QR codes.
+ totpUri:
type: string
- format: date
- description: Date of incorporation in ISO 8601 format (YYYY-MM-DD)
- example: '2018-03-14'
- entityType:
- $ref: '#/components/schemas/EntityType'
- taxId:
+ description: The `otpauth://` provisioning URI (the QR-code payload) the customer's authenticator app scans to enroll the factor.
+ example: otpauth://totp/Grid:customer@example.com?secret=ABC123&issuer=Grid
+ PasskeyEnrollmentStart:
+ type: object
+ description: Opaque WebAuthn registration options relayed to the end user's device to enroll a passkey factor. Grid performs no crypto; pass `options` to the device's WebAuthn API to produce a credential, then submit that credential to the confirm endpoint unmodified.
+ required:
+ - type
+ - options
+ - allowedOrigins
+ - relyingPartyId
+ properties:
+ type:
type: string
- description: Tax identification number
- example: 47-1234567
- countriesOfOperation:
+ enum:
+ - PASSKEY
+ description: Discriminator identifying this as the passkey enrollment-start payload.
+ options:
+ type: object
+ additionalProperties: true
+ description: Opaque WebAuthn `PublicKeyCredentialCreationOptions`. Pass to the device's WebAuthn registration API unmodified.
+ allowedOrigins:
type: array
+ description: The origins the WebAuthn registration ceremony may run against. The origin the credential is produced against must be one of these and must be echoed back on the confirm call.
items:
type: string
- description: List of countries where the business operates (ISO 3166-1 alpha-2)
example:
- - US
- businessType:
- $ref: '#/components/schemas/BusinessType'
- purposeOfAccount:
- $ref: '#/components/schemas/PurposeOfAccount'
- sourceOfFunds:
+ - https://app.example.com
+ relyingPartyId:
type: string
- description: The primary source of funds for the business
- example: Funds derived from customer payments for software services
- expectedMonthlyTransactionCount:
+ description: The WebAuthn relying-party id the credential is bound to.
+ example: app.example.com
+ ScaFactorEnrollStartOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/TotpEnrollmentStart'
+ - $ref: '#/components/schemas/PasskeyEnrollmentStart'
+ discriminator:
+ propertyName: type
+ mapping:
+ TOTP: '#/components/schemas/TotpEnrollmentStart'
+ PASSKEY: '#/components/schemas/PasskeyEnrollmentStart'
+ description: 'The factor-specific material needed to complete enrollment, keyed by `type`: a TOTP shared secret + provisioning URI, or the WebAuthn registration options for a passkey.'
+ TotpEnrollmentConfirmRequest:
+ type: object
+ description: The shared secret returned by the TOTP enrollment start, plus the first code the customer's authenticator app produces, submitted to confirm and finalize the TOTP factor.
+ required:
+ - type
+ - secret
+ - code
+ properties:
+ type:
type: string
enum:
- - COUNT_UNDER_10
- - COUNT_10_TO_100
- - COUNT_100_TO_500
- - COUNT_500_TO_1000
- - COUNT_OVER_1000
- description: Expected number of transactions per month
- example: COUNT_100_TO_500
- expectedMonthlyTransactionVolume:
+ - TOTP
+ description: Discriminator selecting the TOTP confirm variant.
+ secret:
type: string
- enum:
- - VOLUME_UNDER_10K
- - VOLUME_10K_TO_100K
- - VOLUME_100K_TO_1M
- - VOLUME_1M_TO_10M
- - VOLUME_OVER_10M
- description: Expected total transaction volume per month in USD equivalent
- example: VOLUME_100K_TO_1M
- expectedRecipientJurisdictions:
- type: array
- items:
- type: string
- description: List of countries where the business expects to send payments (ISO 3166-1 alpha-2)
- example:
- - US
- naicsCode:
+ description: The shared secret returned as `secret` by the TOTP enrollment start, threaded back to bind the confirmation to that enrollment.
+ code:
type: string
- pattern: ^\d{2,6}$
- description: NAICS code describing the nature of the business (2-6 digits)
- example: '541511'
- sourceOfFundsCategories:
- type: array
- items:
- $ref: '#/components/schemas/SourceOfFundsCategory'
- description: Structured source-of-funds categories for the business
- example:
- - OPERATING_REVENUE
- sourceOfFundsOtherDescription:
+ description: The current time-based one-time code from the customer's authenticator app. In sandbox, the code is always `123456`.
+ example: '123456'
+ PasskeyEnrollmentConfirmRequest:
+ type: object
+ description: The WebAuthn credential a device produced for a passkey registration challenge, submitted to enroll the passkey factor.
+ required:
+ - type
+ - origin
+ - credential
+ properties:
+ type:
type: string
- minLength: 1
- maxLength: 500
- description: Description of the source of funds when OTHER is selected
- example: Proceeds from a legal settlement
- purposeOfAccountOtherDescription:
+ enum:
+ - PASSKEY
+ description: Discriminator selecting the passkey confirm variant.
+ origin:
+ type: string
+ description: The WebAuthn origin the `credential` was produced against (one of the enrollment start's `allowedOrigins`).
+ example: https://app.example.com
+ credential:
+ type: object
+ additionalProperties: true
+ description: Opaque WebAuthn credential the device produced from the enrollment start's `options`.
+ ScaFactorConfirmRequestOneOf:
+ oneOf:
+ - $ref: '#/components/schemas/TotpEnrollmentConfirmRequest'
+ - $ref: '#/components/schemas/PasskeyEnrollmentConfirmRequest'
+ discriminator:
+ propertyName: type
+ mapping:
+ TOTP: '#/components/schemas/TotpEnrollmentConfirmRequest'
+ PASSKEY: '#/components/schemas/PasskeyEnrollmentConfirmRequest'
+ description: 'The proof that finalizes enrollment, keyed by `type`: the TOTP shared secret + code, or the passkey `origin` + `credential`.'
+ TotpEnrollmentConfirmResponse:
+ type: object
+ description: The one-time recovery codes issued once a TOTP factor is enrolled. These are shown to the customer only once; store them somewhere safe to recover access if the authenticator device is lost.
+ required:
+ - type
+ - recoveryCodes
+ properties:
+ type:
type: string
- minLength: 1
- maxLength: 500
- description: Description of the account purpose when OTHER is selected
- example: Escrow for equipment leases
- expectedCounterpartyCountries:
+ enum:
+ - TOTP
+ description: Discriminator identifying this as the TOTP enrollment result.
+ recoveryCodes:
type: array
+ description: The one-time recovery codes for this TOTP factor.
items:
type: string
- description: List of countries of the business's expected transaction counterparties (ISO 3166-1 alpha-2)
example:
- - US
- BusinessCustomerCreateRequest:
- title: Business Customer Create Request
- allOf:
- - $ref: '#/components/schemas/CustomerCreateRequest'
- - $ref: '#/components/schemas/BusinessCustomerFields'
- - type: object
- properties:
- businessInfo:
- $ref: '#/components/schemas/BusinessInfo'
- CustomerCreateRequestOneOf:
+ - ABCD-EFGH-IJKL
+ - MNOP-QRST-UVWX
+ PasskeyEnrollmentConfirmResponse:
+ type: object
+ description: The enrolled passkey factor returned after a successful confirmation.
+ required:
+ - type
+ - factor
+ properties:
+ type:
+ type: string
+ enum:
+ - PASSKEY
+ description: Discriminator identifying this as the passkey enrollment result.
+ factor:
+ $ref: '#/components/schemas/ScaFactorView'
+ ScaFactorConfirmResponseOneOf:
oneOf:
- - $ref: '#/components/schemas/IndividualCustomerCreateRequest'
- - $ref: '#/components/schemas/BusinessCustomerCreateRequest'
+ - $ref: '#/components/schemas/TotpEnrollmentConfirmResponse'
+ - $ref: '#/components/schemas/PasskeyEnrollmentConfirmResponse'
discriminator:
- propertyName: customerType
+ propertyName: type
mapping:
- INDIVIDUAL: '#/components/schemas/IndividualCustomerCreateRequest'
- BUSINESS: '#/components/schemas/BusinessCustomerCreateRequest'
- Error409:
+ TOTP: '#/components/schemas/TotpEnrollmentConfirmResponse'
+ PASSKEY: '#/components/schemas/PasskeyEnrollmentConfirmResponse'
+ description: The enrollment result, keyed by `type`.
+ ScaLoginStartRequest:
type: object
+ description: Selects which enrolled factor to start an SCA login with. The factor must already be enrolled (or, for `SMS_OTP`, the phone verified).
required:
- - message
- - status
- - code
+ - factor
properties:
- status:
- type: integer
- enum:
- - 409
- description: HTTP status code
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor to authenticate with.
+ ScaLoginStart:
+ type: object
+ description: 'The factor-specific material a customer needs to complete an SCA login. Each factor surfaces only the fields it issues: `SMS_OTP` carries `challengeId` and `expiresAt`; `TOTP` carries neither (the customer reads the code from their authenticator app); `PASSKEY` carries the opaque WebAuthn `passkeyOptions` with `allowedOrigins` and `relyingPartyId`.'
+ required:
+ - factor
+ properties:
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor this login was started for.
+ challengeId:
+ type:
+ - string
+ - 'null'
+ description: The challenge handle for an `SMS_OTP` login, threaded back on the complete call. Present only for `SMS_OTP`.
+ expiresAt:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: Absolute UTC timestamp after which the `SMS_OTP` code expires. Present only for `SMS_OTP`.
+ example: '2025-10-03T12:05:00Z'
+ passkeyOptions:
+ type:
+ - object
+ - 'null'
+ additionalProperties: true
+ description: Opaque WebAuthn assertion request options. Present only for `PASSKEY`; pass to the device's WebAuthn API to produce the assertion submitted on the complete call.
+ allowedOrigins:
+ type:
+ - array
+ - 'null'
+ items:
+ type: string
+ description: The origins the WebAuthn ceremony may run against. Present only for `PASSKEY`.
+ example:
+ - https://app.example.com
+ relyingPartyId:
+ type:
+ - string
+ - 'null'
+ description: The WebAuthn relying-party id. Present only for `PASSKEY`.
+ example: app.example.com
+ ScaLoginCompleteRequest:
+ type: object
+ description: Completes an SCA login by submitting the proof for the started factor. Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`), plus the `factor` being completed and, for `SMS_OTP`, the `challengeId` returned by the login start.
+ required:
+ - factor
+ anyOf:
+ - required:
+ - code
+ - required:
+ - passkeyAssertion
+ - origin
+ properties:
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor being completed; must match the started login.
+ challengeId:
+ type:
+ - string
+ - 'null'
+ description: The challenge handle returned by the login start, required for `SMS_OTP` and omitted for other factors.
code:
- type: string
- description: |
- | Error Code | Description |
- |------------|-------------|
- | TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL | Transaction is not pending platform approval |
- | UMA_ADDRESS_EXISTS | UMA address already exists |
- | EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential |
- | EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued |
- | CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform |
- enum:
- - TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL
- - UMA_ADDRESS_EXISTS
- - EMAIL_OTP_EMAIL_ALREADY_EXISTS
- - EMAIL_OTP_CREDENTIAL_SET_CHANGED
- - CONFLICT
- message:
- type: string
- description: Error message
- details:
- type: object
- description: Additional error details
+ type:
+ - string
+ - 'null'
+ description: The one-time code the customer received by SMS, or read from their authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is always `123456`.
+ example: '123456'
+ passkeyAssertion:
+ type:
+ - object
+ - 'null'
additionalProperties: true
- Error404:
+ description: Opaque WebAuthn assertion produced by the device from the login start's `passkeyOptions`. Required when completing a `PASSKEY` login.
+ origin:
+ type:
+ - string
+ - 'null'
+ description: The WebAuthn origin the `passkeyAssertion` was produced against (one of the login start's `allowedOrigins`). Required alongside `passkeyAssertion`; omit it for the `code` path.
+ example: https://app.example.com
+ ScaLoginComplete:
type: object
+ description: The status of a completed SCA login session.
required:
- - message
- status
- - code
properties:
status:
- type: integer
- enum:
- - 404
- description: HTTP status code
- code:
type: string
- description: |
- | Error Code | Description |
- |------------|-------------|
- | TRANSACTION_NOT_FOUND | Transaction not found |
- | INVITATION_NOT_FOUND | Invitation not found |
- | USER_NOT_FOUND | Customer not found |
- | QUOTE_NOT_FOUND | Quote not found |
- | LOOKUP_REQUEST_NOT_FOUND | Lookup request not found |
- | TOKEN_NOT_FOUND | Token not found |
- | BULK_UPLOAD_JOB_NOT_FOUND | Bulk upload job not found |
- | REFERENCE_NOT_FOUND | Reference not found |
- | UMA_NOT_FOUND | The UMA address is well-formed but no receiver exists at the counterparty VASP |
- | STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND | Stablecoin provider account link not found |
- enum:
- - TRANSACTION_NOT_FOUND
- - INVITATION_NOT_FOUND
- - USER_NOT_FOUND
- - QUOTE_NOT_FOUND
- - LOOKUP_REQUEST_NOT_FOUND
- - TOKEN_NOT_FOUND
- - BULK_UPLOAD_JOB_NOT_FOUND
- - REFERENCE_NOT_FOUND
- - UMA_NOT_FOUND
- - STABLECOIN_PROVIDER_ACCOUNT_NOT_FOUND
- message:
- type: string
- description: Error message
- details:
- type: object
- description: Additional error details
- additionalProperties: true
- Error410:
+ description: The status of the login session, passed through verbatim (Grid does not normalize it). A successful login reports `SUCCESS`; other values indicate the login did not complete and should be surfaced to the caller.
+ example: SUCCESS
+ Error423:
type: object
required:
- message
@@ -10793,97 +12217,117 @@ components:
status:
type: integer
enum:
- - 410
+ - 423
description: HTTP status code
code:
type: string
description: |
| Error Code | Description |
|------------|-------------|
- | CUSTOMER_DELETED | Customer has been permanently deleted |
+ | ACCOUNT_LOCKED | The customer's login is temporarily locked (or suspended) after too many failed attempts. Inspect `details.lockedUntil` for when the customer may retry, and `details.failedAttempts` for the current count. |
enum:
- - CUSTOMER_DELETED
+ - ACCOUNT_LOCKED
message:
type: string
description: Error message
details:
type: object
- description: Additional error details
+ description: Lockout detail. `lockedUntil` is the UTC timestamp the customer may retry after (absent when suspended indefinitely); `failedAttempts` is the current cumulative failed-login count; `suspended` is true when the account is locked with no automatic expiry.
+ properties:
+ suspended:
+ type: boolean
+ lockedUntil:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ failedAttempts:
+ type: integer
additionalProperties: true
- CustomerUpdateRequest:
- title: Customer Update Request
- description: Request body for `PATCH /customers/{customerId}`. When `email` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer email and every tied `EMAIL_OTP` credential through the endpoint's signed-retry flow. When `phoneNumber` changes for a customer with tied Embedded Wallet internal accounts, Grid updates the customer phone number and every tied `SMS_OTP` credential through the same signed-retry flow. Update `email` and `phoneNumber` in separate PATCH calls.
+ RecordSecurityEventRequest:
type: object
+ description: Records a client-side security event for the customer so Grid can maintain the customer's login-security state (SCA session revocation and failed-login lockout).
required:
- - customerType
+ - eventType
properties:
- customerType:
- $ref: '#/components/schemas/CustomerType'
- currencies:
- type: array
- items:
- type: string
- description: Updated list of currency codes the customer will use (ISO 4217 for fiat, e.g. "USD", "EUR"; tickers for crypto, e.g. "BTC", "USDC"). Replaces the existing list. Some currency combinations may require separate customers — if so, the request will be rejected with details.
- example:
- - USD
- - EUR
- - USDC
- email:
- type: string
- format: email
- description: Email address for the customer. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `EMAIL_OTP` credential across all tied Embedded Wallets.
- example: john.doe@example.com
- phoneNumber:
+ eventType:
type: string
- pattern: ^\+[1-9]\d{1,14}$
- description: Phone number for the customer in strict E.164 format. For customers with tied Embedded Wallet internal accounts, changing this value also updates every tied `SMS_OTP` credential across all tied Embedded Wallets. Send phone number and email updates as separate PATCH calls.
- example: '+14155551234'
- umaAddress:
- type: string
- description: Optional UMA address identifier. If provided, the customer's UMA address will be updated. This is an optional identifier to route payments to the customer.
- example: $john.doe@uma.domain.com
- IndividualCustomerUpdateRequest:
- title: Individual Customer Update Request
- allOf:
- - $ref: '#/components/schemas/CustomerUpdateRequest'
- - $ref: '#/components/schemas/IndividualCustomerFields'
- BusinessCustomerUpdateRequest:
- title: Business Customer Update Request
- allOf:
- - $ref: '#/components/schemas/CustomerUpdateRequest'
- - $ref: '#/components/schemas/BusinessCustomerFields'
- CustomerUpdateRequestOneOf:
- oneOf:
- - $ref: '#/components/schemas/IndividualCustomerUpdateRequest'
- - $ref: '#/components/schemas/BusinessCustomerUpdateRequest'
- discriminator:
- propertyName: customerType
- mapping:
- INDIVIDUAL: '#/components/schemas/IndividualCustomerUpdateRequest'
- BUSINESS: '#/components/schemas/BusinessCustomerUpdateRequest'
- SignedRequestChallenge:
- title: Signed Request Challenge
+ enum:
+ - RESET_PASSWORD_COMPLETED
+ - FAILED_LOGIN_ATTEMPT
+ description: |
+ The security event to record:
+
+ | Value | Effect |
+ |-------|--------|
+ | `RESET_PASSWORD_COMPLETED` | Revokes every active SCA session for the customer and clears the failed-login counter. |
+ | `FAILED_LOGIN_ATTEMPT` | Increments the cumulative failed-login counter and escalates the lockout at each milestone: 5 attempts → 15 minutes, 6 → 30 minutes, 7 → 1 hour, 8 → 24 hours, 9 or more → suspension. |
+ example: FAILED_LOGIN_ATTEMPT
+ RecordSecurityEventResponse:
type: object
+ description: The customer's login-security state after recording the event, so the integrator can surface a lockout to the end user.
required:
- - payloadToSign
- - requestId
- - expiresAt
- description: Common base for two-step signed-retry challenge responses on Embedded Wallet endpoints (credential registration or revocation, session refresh or revocation, wallet export, customer email updates, and similar). Holds the signing fields shared across every challenge shape; each variant composes this base via `allOf` and adds its own resource `id` (and `type`, when applicable) with variant-specific description and example.
+ - eventType
+ - suspended
+ - failedAttempts
properties:
- payloadToSign:
+ eventType:
type: string
- description: Canonical payload for the retry authorization stamp. Build an API-key stamp over this exact value with the session API keypair, then send the full base64url-encoded stamp in `Grid-Wallet-Signature` on the retry that completes the original request.
- example: '{"organizationId":"org_2m9F...","parameters":{"userId":"user_2m9F..."},"timestampMs":"1775681700000","type":"ACTIVITY_TYPE_EXAMPLE"}'
- requestId:
+ enum:
+ - RESET_PASSWORD_COMPLETED
+ - FAILED_LOGIN_ATTEMPT
+ description: The event that was recorded.
+ example: FAILED_LOGIN_ATTEMPT
+ suspended:
+ type: boolean
+ description: Whether the customer's login is currently suspended (locked with no automatic expiry). A suspended customer must go through a password reset (`RESET_PASSWORD_COMPLETED`) to clear the lockout.
+ lockedUntil:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: When the customer may attempt to log in again, if temporarily locked. Null when not locked, or when `suspended` is true (no automatic expiry).
+ example: '2025-10-03T12:15:00Z'
+ failedAttempts:
+ type: integer
+ description: The customer's current cumulative failed-login count.
+ example: 5
+ TwoFactorResetStartRequest:
+ type: object
+ description: Selects which enrolled factor to reset via the liveness-gated recovery flow.
+ required:
+ - factor
+ properties:
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The enrolled factor to reset.
+ TwoFactorResetStart:
+ type: object
+ description: The reset handle plus the opaque liveness handles a caller relays to the end-user device to complete the liveness check. `resetId` threads the ceremony together (status and complete reference it). `livenessAccessToken` and `verificationLink` are omitted when they are not returned.
+ required:
+ - resetId
+ properties:
+ resetId:
type: string
- description: Grid-issued `Request:` identifier for this pending request. Echo this value exactly in the `Request-Id` header on the signed retry so the server can correlate the retry with the issued challenge.
- example: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
+ description: Identifier for this reset; pass it to the status and complete endpoints.
+ livenessAccessToken:
+ type:
+ - string
+ - 'null'
+ description: Access token for the embedded liveness/verification SDK, bound to this reset. Omitted when one is not returned.
+ verificationLink:
+ type:
+ - string
+ - 'null'
+ description: Hosted identity-verification page URL for completing liveness. Omitted when one is not returned.
expiresAt:
- type: string
+ type:
+ - string
+ - 'null'
format: date-time
- description: Timestamp after which this challenge is no longer valid. The signed retry must be submitted before this time.
- example: '2026-04-08T15:35:00Z'
- Error424:
+ description: Absolute UTC timestamp at the end of the reset window. Omitted when one is not returned.
+ example: '2025-10-03T12:30:00Z'
+ Error429:
type: object
required:
- message
@@ -10893,24 +12337,16 @@ components:
status:
type: integer
enum:
- - 424
+ - 429
description: HTTP status code
code:
type: string
description: |
| Error Code | Description |
|------------|-------------|
- | PAYREQ_REQUEST_FAILED | Payment request failed |
- | COUNTERPARTY_PUBKEY_FETCH_ERROR | Error fetching counterparty public key |
- | NO_COMPATIBLE_UMA_VERSION | No compatible UMA version |
- | LNURLP_REQUEST_FAILED | LNURLP request failed |
- | EMAIL_OTP_CREDENTIAL_SYNC_FAILED | Failed to update one or more tied EMAIL_OTP credentials |
+ | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header |
enum:
- - PAYREQ_REQUEST_FAILED
- - COUNTERPARTY_PUBKEY_FETCH_ERROR
- - NO_COMPATIBLE_UMA_VERSION
- - LNURLP_REQUEST_FAILED
- - EMAIL_OTP_CREDENTIAL_SYNC_FAILED
+ - RATE_LIMITED
message:
type: string
description: Error message
@@ -10918,54 +12354,175 @@ components:
type: object
description: Additional error details
additionalProperties: true
- KycLinkCreateRequest:
+ TwoFactorResetStatus:
type: object
- description: Request body for generating a hosted KYC link for an existing customer.
+ description: The status of an in-progress 2FA reset, polled until it reaches a terminal value.
+ required:
+ - status
+ - factor
+ - expiresAt
properties:
- redirectUri:
+ status:
type: string
- format: uri
- description: URI the customer is redirected to after completing the hosted KYC flow. Must start with `https://` (or `http://` for local development). Embedded in the returned `kycUrl`.
- example: https://app.example.com/onboarding/completed
- KycProvider:
- type: string
- description: The KYC provider that will perform identity verification for the customer. Grid selects the provider based on the customer's region and platform configuration; the value is informational for platforms that want to integrate directly with the provider's SDK.
- enum:
- - SUMSUB
- example: SUMSUB
- KycLinkResponse:
+ enum:
+ - INITIATED
+ - PENDING_REVIEW
+ - LIVENESS_PASSED
+ - COMPLETED
+ - REJECTED
+ - EXPIRED
+ description: |-
+ The reset status.
+ | Value | Terminal | Meaning | |-------|----------|---------| | `INITIATED` | no | Reset started; liveness not yet submitted. Keep polling. | | `PENDING_REVIEW` | no | Liveness submitted; under review. Keep polling. | | `LIVENESS_PASSED` | no | Liveness passed; call the complete endpoint to finish the reset. | | `COMPLETED` | **yes** | Reset finished; the lost factor is cleared and re-enrollment can begin. | | `REJECTED` | **yes** | Liveness failed. Stop polling; start a new reset. | | `EXPIRED` | **yes** | The reset window closed before it completed. Stop polling; start a new reset. |
+ Stop polling on any terminal value.
+ example: INITIATED
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor being reset.
+ enrollmentStatus:
+ type:
+ - string
+ - 'null'
+ enum:
+ - PENDING
+ - COMPLETED
+ - null
+ description: The enrollment status of the replacement factor. `PENDING` until the customer finishes re-enrolling, then `COMPLETED`. Null for an `SMS_OTP` reset, where completing the reset enrolls the new number directly with no separate enrollment step.
+ example: PENDING
+ expiresAt:
+ type: string
+ format: date-time
+ description: When the reset window closes. Poll no longer than this.
+ example: '2025-10-03T12:15:00Z'
+ completedAt:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: When the reset completed; null until then.
+ example: null
+ TwoFactorResetCompleteRequest:
type: object
- description: A hosted KYC link that the customer can complete to verify their identity.
+ description: Optional body for completing a 2FA reset. Only needed when resetting the `SMS_OTP` factor to a new phone number; omit the body entirely otherwise.
+ properties:
+ mobile:
+ type: object
+ description: For an `SMS_OTP` reset, the new mobile number to enroll as the customer completes the reset. Required for an `SMS_OTP` reset; ignored for `TOTP` / `PASSKEY` resets.
+ required:
+ - countryCode
+ - number
+ properties:
+ countryCode:
+ type: string
+ description: The country dialing code, including the leading `+`.
+ example: '+1'
+ number:
+ type: string
+ description: The national subscriber number, without the country code.
+ example: '4155550123'
+ ScaChallenge:
+ type: object
+ description: |-
+ A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed.
+
+ When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`.
+
+ **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged.
required:
- - kycUrl
+ - id
- expiresAt
- - provider
+ - factor
+ - availableFactors
properties:
- kycUrl:
+ id:
type: string
- description: Hosted URL the customer should be sent to in order to complete verification. The URL is single-use and expires at `expiresAt`. To generate a new link (for example, after the previous one expires or is abandoned), call this endpoint again.
- example: https://kyc.lightspark.com/onboard/abc123def456
+ description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation).
+ example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007
expiresAt:
type: string
format: date-time
- description: Time at which the hosted link expires and can no longer be used.
- example: '2027-01-15T14:32:00Z'
- provider:
- $ref: '#/components/schemas/KycProvider'
- token:
- type: string
- description: Provider-specific token that can be used in place of the hosted URL — for example, to embed the provider's SDK directly in your application. Only returned for providers that support direct SDK integration. Whether to use the hosted URL or the embedded SDK is up to you; both flows result in the same `kycStatus` update on the customer.
- example: _act-sbx-jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- ContactVerificationConfirmRequest:
+ description: Absolute UTC timestamp after which this challenge can no longer be authorized.
+ example: '2025-10-03T12:05:00Z'
+ factor:
+ $ref: '#/components/schemas/ScaFactor'
+ description: The factor this challenge was issued for. Defaults to `SMS_OTP`.
+ availableFactors:
+ type: array
+ description: The factors the customer may use to satisfy this challenge.
+ items:
+ $ref: '#/components/schemas/ScaFactor'
+ example:
+ - SMS_OTP
+ purpose:
+ type:
+ - string
+ - 'null'
+ description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow).
+ example: PAYOUT
+ passkeyAssertionOptions:
+ type:
+ - object
+ - 'null'
+ additionalProperties: true
+ description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`.
+ passkeyAllowedOrigins:
+ type:
+ - array
+ - 'null'
+ items:
+ type: string
+ description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case.
+ example:
+ - https://app.example.com
+ BeneficiaryTrustStart:
type: object
- description: Request body for confirming an email or phone verification challenge.
- required:
- - code
+ description: The SCA challenge (if any) a caller authorizes to finish trusting (or untrusting) a beneficiary. The beneficiary is identified by its `externalAccountId`, so the confirm call needs no separate handle. `scaChallenge` is omitted when no challenge is issued; the caller then confirms without a `challengeId`.
+ properties:
+ scaChallenge:
+ $ref: '#/components/schemas/ScaChallenge'
+ description: The SCA challenge to satisfy on the confirm call. Omitted when no challenge is issued.
+ BeneficiaryTrustConfirmRequest:
+ type: object
+ description: Confirms trusting or untrusting a beneficiary by submitting the SCA proof. Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`) and, when the start issued one, the `challengeId`. The beneficiary is identified by the `externalAccountId` in the path — no separate handle is needed.
+ anyOf:
+ - required:
+ - code
+ - required:
+ - passkeyAssertion
+ - origin
properties:
+ challengeId:
+ type:
+ - string
+ - 'null'
+ description: The challenge handle from the trust start's `scaChallenge`, when one was issued. Omit when the start returned no challenge.
code:
- type: string
- description: The one-time verification code the customer received via email or SMS. In sandbox, the code is always `123456`.
+ type:
+ - string
+ - 'null'
+ description: The one-time code the customer received by SMS, or read from their authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is always `123456`.
example: '123456'
+ passkeyAssertion:
+ type:
+ - object
+ - 'null'
+ additionalProperties: true
+ description: Opaque WebAuthn assertion produced by the device from the challenge's assertion options. Required when satisfying a `PASSKEY` challenge.
+ origin:
+ type:
+ - string
+ - 'null'
+ description: The WebAuthn origin the `passkeyAssertion` was produced against. Required alongside `passkeyAssertion`; omit it for the `code` path.
+ example: https://app.example.com
+ BeneficiaryTrustConfirm:
+ type: object
+ description: The result of a confirm-trust / confirm-untrust call. `trusted` is `true` after a successful trust and `false` after a successful untrust.
+ required:
+ - trusted
+ properties:
+ trusted:
+ type: boolean
+ description: Whether the beneficiary is now trusted. `true` after a successful trust, `false` after a successful untrust.
InternalAccountType:
title: Internal Account Type
type: string
@@ -18202,20 +19759,6 @@ components:
description: The payment rail to use for the transfer. Must be one of the rails supported by the destination account. If not specified, the system will select a default rail.
allOf:
- $ref: '#/components/schemas/PaymentRail'
- ScaFactor:
- type: string
- enum:
- - SMS_OTP
- - TOTP
- - PASSKEY
- description: |
- A Strong Customer Authentication factor.
-
- | Factor | Description |
- |--------|-------------|
- | `SMS_OTP` | One-time code sent by SMS to the customer's verified phone. Requires no prior enrollment. |
- | `TOTP` | Time-based one-time code from an authenticator app. Requires enrollment. Not valid for per-transaction challenges (cannot carry dynamic linking). |
- | `PASSKEY` | WebAuthn passkey assertion. Requires enrollment. |
TransferOutRequest:
type: object
required:
@@ -18238,9 +19781,6 @@ components:
maxLength: 80
description: 'Free-form information about the payment that travels with it to the recipient. The field this populates depends on the payment rail: for ACH it populates the Addenda record, for FedNow and RTP it populates the remittanceInformation field, and for wires it populates the OBI (Originator to Beneficiary Information) / beneficiary information.'
example: '12345'
- scaFactor:
- $ref: '#/components/schemas/ScaFactor'
- description: Optional preferred factor for the Strong Customer Authentication challenge this call issues. Only relevant for customers in a region where SCA is required (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required dynamic linking and is rejected here. Omit to default to `SMS_OTP`.
CurrencyPreference:
type: object
required:
@@ -18475,60 +20015,6 @@ components:
mapping:
ACCOUNT: '#/components/schemas/AccountDestination'
UMA_ADDRESS: '#/components/schemas/UmaAddressDestination'
- ScaChallenge:
- type: object
- description: |-
- A Strong Customer Authentication challenge that must be satisfied before a money-movement operation can complete. This object is **only present when the customer is in a region where SCA is required** (the EU); for customers outside SCA-regulated regions it is omitted entirely and no action is needed.
-
- When present on a quote, authorize it by submitting an `ScaAuthorization` proof to `POST /quotes/{quoteId}/authorize`.
-
- **A single operation may require more than one authorization, in sequence.** Treat `scaChallenge` as *the challenge to satisfy now*, not "the only one". After each authorize, re-inspect the returned quote/transaction: if it is still `PENDING_AUTHORIZATION`, it carries the **next** `scaChallenge` (a new `id`) — authorize that too, and repeat until it leaves `PENDING_AUTHORIZATION`. Do not assume one authorization releases the transfer. The number of authorizations is flow-dependent and **may decrease in future**: for example, a cross-currency send today authorizes the currency conversion and the payout as two separate challenges; a future update may collapse them into one. A client written to loop on status handles any count unchanged.
- required:
- - id
- - expiresAt
- - factor
- - availableFactors
- properties:
- id:
- type: string
- description: Unique identifier for this challenge. The server resolves the active challenge from the quote or transaction being authorized, so this field need not be supplied back; it is informational (e.g. for logging or correlation).
- example: ScaChallenge:019542f5-b3e7-1d02-0000-000000000007
- expiresAt:
- type: string
- format: date-time
- description: Absolute UTC timestamp after which this challenge can no longer be authorized.
- example: '2025-10-03T12:05:00Z'
- factor:
- $ref: '#/components/schemas/ScaFactor'
- description: The factor this challenge was issued for. Defaults to `SMS_OTP`.
- availableFactors:
- type: array
- description: The factors the customer may use to satisfy this challenge.
- items:
- $ref: '#/components/schemas/ScaFactor'
- example:
- - SMS_OTP
- purpose:
- type:
- - string
- - 'null'
- description: Optional, informational label for what this particular challenge in the sequence authorizes — useful for step UX (e.g. "Authorize the currency conversion" vs "Authorize the payout"). Known values include `CURRENCY_CONVERSION`, `PAYOUT`, and `TRANSFER`, but the set is **non-exhaustive and may grow** — treat unrecognized values as a generic authorization step and do not branch program logic on it. Omitted when steps are not distinguished (e.g. a single-authorization flow).
- example: PAYOUT
- passkeyAssertionOptions:
- type:
- - object
- - 'null'
- additionalProperties: true
- description: Opaque WebAuthn assertion request options (including the relying-party id, challenge, and allowed credentials), present only when `factor` is `PASSKEY`. Pass to the device's WebAuthn API to produce the assertion submitted back in `ScaAuthorization.passkeyAssertion`.
- passkeyAllowedOrigins:
- type:
- - array
- - 'null'
- items:
- type: string
- description: The origins the WebAuthn ceremony may run against. Populated for enrollment and login passkey challenges; the origin the assertion is produced against must be one of these and echoed back as `ScaAuthorization.origin`. Per-transaction passkey challenges omit this (they carry `passkeyAssertionOptions` only) — see `ScaAuthorization.origin` for how to source the origin in that case.
- example:
- - https://app.example.com
Quote:
type: object
required:
@@ -18753,33 +20239,6 @@ components:
- 'null'
description: The WebAuthn origin the `passkeyAssertion` was produced against. **Required** alongside `passkeyAssertion`; omit it for the `code` path. When the challenge lists `passkeyAllowedOrigins` (enrollment / login challenges), it must be one of those. A per-transaction passkey challenge carries `passkeyAssertionOptions` but may omit `passkeyAllowedOrigins`; in that case supply the origin your app invoked the WebAuthn API from, which must match the relying party in `passkeyAssertionOptions`.
example: https://app.example.com
- Error429:
- type: object
- required:
- - message
- - status
- - code
- properties:
- status:
- type: integer
- enum:
- - 429
- description: HTTP status code
- code:
- type: string
- description: |
- | Error Code | Description |
- |------------|-------------|
- | RATE_LIMITED | Too many requests in a short window; retry after the interval indicated by the `Retry-After` response header |
- enum:
- - RATE_LIMITED
- message:
- type: string
- description: Error message
- details:
- type: object
- description: Additional error details
- additionalProperties: true
TransactionListResponse:
type: object
required:
diff --git a/openapi/components/schemas/errors/Error409.yaml b/openapi/components/schemas/errors/Error409.yaml
index 09db54cbe..4bc53ad27 100644
--- a/openapi/components/schemas/errors/Error409.yaml
+++ b/openapi/components/schemas/errors/Error409.yaml
@@ -18,12 +18,14 @@ properties:
| UMA_ADDRESS_EXISTS | UMA address already exists |
| EMAIL_OTP_EMAIL_ALREADY_EXISTS | Email address is already associated with an EMAIL_OTP credential |
| EMAIL_OTP_CREDENTIAL_SET_CHANGED | Tied EMAIL_OTP credential set changed after the signed-retry challenge was issued |
+ | PASSKEY_ALREADY_ENROLLED | The customer already has an enrolled passkey factor; only one passkey per customer is supported. Delete the existing one before enrolling another |
| CONFLICT | Generic resource-state conflict. Returned, for example, when `platformCustomerId` on a customer create call collides with an existing active customer on the same platform |
enum:
- TRANSACTION_NOT_PENDING_PLATFORM_APPROVAL
- UMA_ADDRESS_EXISTS
- EMAIL_OTP_EMAIL_ALREADY_EXISTS
- EMAIL_OTP_CREDENTIAL_SET_CHANGED
+ - PASSKEY_ALREADY_ENROLLED
- CONFLICT
message:
type: string
diff --git a/openapi/components/schemas/errors/Error423.yaml b/openapi/components/schemas/errors/Error423.yaml
new file mode 100644
index 000000000..63d80498b
--- /dev/null
+++ b/openapi/components/schemas/errors/Error423.yaml
@@ -0,0 +1,40 @@
+type: object
+required:
+ - message
+ - status
+ - code
+properties:
+ status:
+ type: integer
+ enum:
+ - 423
+ description: HTTP status code
+ code:
+ type: string
+ description: |
+ | Error Code | Description |
+ |------------|-------------|
+ | ACCOUNT_LOCKED | The customer's login is temporarily locked (or suspended) after too many failed attempts. Inspect `details.lockedUntil` for when the customer may retry, and `details.failedAttempts` for the current count. |
+ enum:
+ - ACCOUNT_LOCKED
+ message:
+ type: string
+ description: Error message
+ details:
+ type: object
+ description: >-
+ Lockout detail. `lockedUntil` is the UTC timestamp the customer may retry
+ after (absent when suspended indefinitely); `failedAttempts` is the current
+ cumulative failed-login count; `suspended` is true when the account is
+ locked with no automatic expiry.
+ properties:
+ suspended:
+ type: boolean
+ lockedUntil:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ failedAttempts:
+ type: integer
+ additionalProperties: true
diff --git a/openapi/components/schemas/sca/BeneficiaryTrustConfirm.yaml b/openapi/components/schemas/sca/BeneficiaryTrustConfirm.yaml
new file mode 100644
index 000000000..beb40af35
--- /dev/null
+++ b/openapi/components/schemas/sca/BeneficiaryTrustConfirm.yaml
@@ -0,0 +1,12 @@
+type: object
+description: >-
+ The result of a confirm-trust / confirm-untrust call. `trusted` is `true`
+ after a successful trust and `false` after a successful untrust.
+required:
+ - trusted
+properties:
+ trusted:
+ type: boolean
+ description: >-
+ Whether the beneficiary is now trusted. `true` after a successful trust,
+ `false` after a successful untrust.
diff --git a/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml
new file mode 100644
index 000000000..686da5819
--- /dev/null
+++ b/openapi/components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml
@@ -0,0 +1,46 @@
+type: object
+description: >-
+ Confirms trusting or untrusting a beneficiary by submitting the SCA proof.
+ Carries the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` /
+ `TOTP`, or `passkeyAssertion` + `origin` for `PASSKEY`) and, when the start
+ issued one, the `challengeId`. The beneficiary is identified by the
+ `externalAccountId` in the path — no separate handle is needed.
+anyOf:
+ - required:
+ - code
+ - required:
+ - passkeyAssertion
+ - origin
+properties:
+ challengeId:
+ type:
+ - string
+ - 'null'
+ description: >-
+ The challenge handle from the trust start's `scaChallenge`, when one was
+ issued. Omit when the start returned no challenge.
+ code:
+ type:
+ - string
+ - 'null'
+ description: >-
+ The one-time code the customer received by SMS, or read from their
+ authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is
+ always `123456`.
+ example: '123456'
+ passkeyAssertion:
+ type:
+ - object
+ - 'null'
+ additionalProperties: true
+ description: >-
+ Opaque WebAuthn assertion produced by the device from the challenge's
+ assertion options. Required when satisfying a `PASSKEY` challenge.
+ origin:
+ type:
+ - string
+ - 'null'
+ description: >-
+ The WebAuthn origin the `passkeyAssertion` was produced against. Required
+ alongside `passkeyAssertion`; omit it for the `code` path.
+ example: https://app.example.com
diff --git a/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml b/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml
new file mode 100644
index 000000000..85d2565d6
--- /dev/null
+++ b/openapi/components/schemas/sca/BeneficiaryTrustStart.yaml
@@ -0,0 +1,13 @@
+type: object
+description: >-
+ The SCA challenge (if any) a caller authorizes to finish trusting (or
+ untrusting) a beneficiary. The beneficiary is identified by its
+ `externalAccountId`, so the confirm call needs no separate handle.
+ `scaChallenge` is omitted when no challenge is issued; the caller then confirms
+ without a `challengeId`.
+properties:
+ scaChallenge:
+ $ref: ./ScaChallenge.yaml
+ description: >-
+ The SCA challenge to satisfy on the confirm call. Omitted when no challenge
+ is issued.
diff --git a/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml
new file mode 100644
index 000000000..8a514ff86
--- /dev/null
+++ b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmRequest.yaml
@@ -0,0 +1,26 @@
+type: object
+description: >-
+ The WebAuthn credential a device produced for a passkey registration
+ challenge, submitted to enroll the passkey factor.
+required:
+ - type
+ - origin
+ - credential
+properties:
+ type:
+ type: string
+ enum:
+ - PASSKEY
+ description: Discriminator selecting the passkey confirm variant.
+ origin:
+ type: string
+ description: >-
+ The WebAuthn origin the `credential` was produced against (one of the
+ enrollment start's `allowedOrigins`).
+ example: https://app.example.com
+ credential:
+ type: object
+ additionalProperties: true
+ description: >-
+ Opaque WebAuthn credential the device produced from the enrollment start's
+ `options`.
diff --git a/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml
new file mode 100644
index 000000000..b74b86298
--- /dev/null
+++ b/openapi/components/schemas/sca/PasskeyEnrollmentConfirmResponse.yaml
@@ -0,0 +1,13 @@
+type: object
+description: The enrolled passkey factor returned after a successful confirmation.
+required:
+ - type
+ - factor
+properties:
+ type:
+ type: string
+ enum:
+ - PASSKEY
+ description: Discriminator identifying this as the passkey enrollment result.
+ factor:
+ $ref: ./ScaFactorView.yaml
diff --git a/openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml b/openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml
new file mode 100644
index 000000000..952799450
--- /dev/null
+++ b/openapi/components/schemas/sca/PasskeyEnrollmentStart.yaml
@@ -0,0 +1,37 @@
+type: object
+description: >-
+ Opaque WebAuthn registration options relayed to the end user's device to
+ enroll a passkey factor. Grid performs no crypto; pass `options` to the
+ device's WebAuthn API to produce a credential, then submit that credential to
+ the confirm endpoint unmodified.
+required:
+ - type
+ - options
+ - allowedOrigins
+ - relyingPartyId
+properties:
+ type:
+ type: string
+ enum:
+ - PASSKEY
+ description: Discriminator identifying this as the passkey enrollment-start payload.
+ options:
+ type: object
+ additionalProperties: true
+ description: >-
+ Opaque WebAuthn `PublicKeyCredentialCreationOptions`. Pass to the device's
+ WebAuthn registration API unmodified.
+ allowedOrigins:
+ type: array
+ description: >-
+ The origins the WebAuthn registration ceremony may run against. The origin
+ the credential is produced against must be one of these and must be echoed
+ back on the confirm call.
+ items:
+ type: string
+ example:
+ - https://app.example.com
+ relyingPartyId:
+ type: string
+ description: The WebAuthn relying-party id the credential is bound to.
+ example: app.example.com
diff --git a/openapi/components/schemas/sca/PasskeyFactorEnrollRequest.yaml b/openapi/components/schemas/sca/PasskeyFactorEnrollRequest.yaml
new file mode 100644
index 000000000..121788885
--- /dev/null
+++ b/openapi/components/schemas/sca/PasskeyFactorEnrollRequest.yaml
@@ -0,0 +1,11 @@
+type: object
+title: Passkey Factor Enroll Request
+description: Start enrolling a WebAuthn passkey factor.
+required:
+ - type
+properties:
+ type:
+ type: string
+ enum:
+ - PASSKEY
+ description: Discriminator selecting the passkey factor. Passkey enrollment needs no other input at start.
diff --git a/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml
new file mode 100644
index 000000000..0ba668d80
--- /dev/null
+++ b/openapi/components/schemas/sca/RecordSecurityEventRequest.yaml
@@ -0,0 +1,21 @@
+type: object
+description: >-
+ Records a client-side security event for the customer so Grid can maintain the
+ customer's login-security state (SCA session revocation and failed-login
+ lockout).
+required:
+ - eventType
+properties:
+ eventType:
+ type: string
+ enum:
+ - RESET_PASSWORD_COMPLETED
+ - FAILED_LOGIN_ATTEMPT
+ description: |
+ The security event to record:
+
+ | Value | Effect |
+ |-------|--------|
+ | `RESET_PASSWORD_COMPLETED` | Revokes every active SCA session for the customer and clears the failed-login counter. |
+ | `FAILED_LOGIN_ATTEMPT` | Increments the cumulative failed-login counter and escalates the lockout at each milestone: 5 attempts → 15 minutes, 6 → 30 minutes, 7 → 1 hour, 8 → 24 hours, 9 or more → suspension. |
+ example: FAILED_LOGIN_ATTEMPT
diff --git a/openapi/components/schemas/sca/RecordSecurityEventResponse.yaml b/openapi/components/schemas/sca/RecordSecurityEventResponse.yaml
new file mode 100644
index 000000000..b0e9a0947
--- /dev/null
+++ b/openapi/components/schemas/sca/RecordSecurityEventResponse.yaml
@@ -0,0 +1,35 @@
+type: object
+description: >-
+ The customer's login-security state after recording the event, so the
+ integrator can surface a lockout to the end user.
+required:
+ - eventType
+ - suspended
+ - failedAttempts
+properties:
+ eventType:
+ type: string
+ enum:
+ - RESET_PASSWORD_COMPLETED
+ - FAILED_LOGIN_ATTEMPT
+ description: The event that was recorded.
+ example: FAILED_LOGIN_ATTEMPT
+ suspended:
+ type: boolean
+ description: >-
+ Whether the customer's login is currently suspended (locked with no
+ automatic expiry). A suspended customer must go through a password reset
+ (`RESET_PASSWORD_COMPLETED`) to clear the lockout.
+ lockedUntil:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: >-
+ When the customer may attempt to log in again, if temporarily locked.
+ Null when not locked, or when `suspended` is true (no automatic expiry).
+ example: '2025-10-03T12:15:00Z'
+ failedAttempts:
+ type: integer
+ description: The customer's current cumulative failed-login count.
+ example: 5
diff --git a/openapi/components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml b/openapi/components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml
new file mode 100644
index 000000000..e8b9cbcba
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml
@@ -0,0 +1,11 @@
+oneOf:
+ - $ref: ./TotpEnrollmentConfirmRequest.yaml
+ - $ref: ./PasskeyEnrollmentConfirmRequest.yaml
+discriminator:
+ propertyName: type
+ mapping:
+ TOTP: ./TotpEnrollmentConfirmRequest.yaml
+ PASSKEY: ./PasskeyEnrollmentConfirmRequest.yaml
+description: >-
+ The proof that finalizes enrollment, keyed by `type`: the TOTP shared secret +
+ code, or the passkey `origin` + `credential`.
diff --git a/openapi/components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml b/openapi/components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml
new file mode 100644
index 000000000..021804cd9
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml
@@ -0,0 +1,9 @@
+oneOf:
+ - $ref: ./TotpEnrollmentConfirmResponse.yaml
+ - $ref: ./PasskeyEnrollmentConfirmResponse.yaml
+discriminator:
+ propertyName: type
+ mapping:
+ TOTP: ./TotpEnrollmentConfirmResponse.yaml
+ PASSKEY: ./PasskeyEnrollmentConfirmResponse.yaml
+description: The enrollment result, keyed by `type`.
diff --git a/openapi/components/schemas/sca/ScaFactorEnrollRequestOneOf.yaml b/openapi/components/schemas/sca/ScaFactorEnrollRequestOneOf.yaml
new file mode 100644
index 000000000..d464c3aab
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaFactorEnrollRequestOneOf.yaml
@@ -0,0 +1,12 @@
+oneOf:
+ - $ref: ./TotpFactorEnrollRequest.yaml
+ - $ref: ./PasskeyFactorEnrollRequest.yaml
+discriminator:
+ propertyName: type
+ mapping:
+ TOTP: ./TotpFactorEnrollRequest.yaml
+ PASSKEY: ./PasskeyFactorEnrollRequest.yaml
+description: >-
+ Which SCA factor to begin enrolling, selected by `type`. `SMS_OTP` is not
+ enrollable (it uses the customer's verified phone), so only `TOTP` and
+ `PASSKEY` are valid here.
diff --git a/openapi/components/schemas/sca/ScaFactorEnrollStartOneOf.yaml b/openapi/components/schemas/sca/ScaFactorEnrollStartOneOf.yaml
new file mode 100644
index 000000000..b216b393c
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaFactorEnrollStartOneOf.yaml
@@ -0,0 +1,12 @@
+oneOf:
+ - $ref: ./TotpEnrollmentStart.yaml
+ - $ref: ./PasskeyEnrollmentStart.yaml
+discriminator:
+ propertyName: type
+ mapping:
+ TOTP: ./TotpEnrollmentStart.yaml
+ PASSKEY: ./PasskeyEnrollmentStart.yaml
+description: >-
+ The factor-specific material needed to complete enrollment, keyed by `type`:
+ a TOTP shared secret + provisioning URI, or the WebAuthn registration options
+ for a passkey.
diff --git a/openapi/components/schemas/sca/ScaFactorList.yaml b/openapi/components/schemas/sca/ScaFactorList.yaml
new file mode 100644
index 000000000..a02cac4e3
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaFactorList.yaml
@@ -0,0 +1,10 @@
+type: object
+description: The Strong Customer Authentication factors a customer has enrolled.
+required:
+ - factors
+properties:
+ factors:
+ type: array
+ description: The customer's enrolled SCA factors.
+ items:
+ $ref: ./ScaFactorView.yaml
diff --git a/openapi/components/schemas/sca/ScaFactorView.yaml b/openapi/components/schemas/sca/ScaFactorView.yaml
new file mode 100644
index 000000000..1a434f37d
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaFactorView.yaml
@@ -0,0 +1,17 @@
+type: object
+description: An enrolled Strong Customer Authentication factor.
+required:
+ - factor
+properties:
+ factor:
+ $ref: ./ScaFactor.yaml
+ description: The kind of enrolled factor.
+ credentialId:
+ type: string
+ description: >-
+ The per-credential id, populated only for `PASSKEY` factors (the id passed
+ to delete a passkey). Omitted for `TOTP` and `SMS_OTP`, which have no
+ per-credential id.
+ name:
+ type: string
+ description: An optional human-readable label for this factor.
diff --git a/openapi/components/schemas/sca/ScaLoginComplete.yaml b/openapi/components/schemas/sca/ScaLoginComplete.yaml
new file mode 100644
index 000000000..e81240c45
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaLoginComplete.yaml
@@ -0,0 +1,13 @@
+type: object
+description: The status of a completed SCA login session.
+required:
+ - status
+properties:
+ status:
+ type: string
+ description: >-
+ The status of the login session, passed through
+ verbatim (Grid does not normalize it). A successful login reports
+ `SUCCESS`; other values indicate the login did not
+ complete and should be surfaced to the caller.
+ example: SUCCESS
diff --git a/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml b/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml
new file mode 100644
index 000000000..cdc1440ca
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaLoginCompleteRequest.yaml
@@ -0,0 +1,51 @@
+type: object
+description: >-
+ Completes an SCA login by submitting the proof for the started factor. Carries
+ the same proof fields as an `ScaAuthorization` (`code` for `SMS_OTP` / `TOTP`,
+ or `passkeyAssertion` + `origin` for `PASSKEY`), plus the `factor` being
+ completed and, for `SMS_OTP`, the `challengeId` returned by the login start.
+required:
+ - factor
+anyOf:
+ - required:
+ - code
+ - required:
+ - passkeyAssertion
+ - origin
+properties:
+ factor:
+ $ref: ./ScaFactor.yaml
+ description: The factor being completed; must match the started login.
+ challengeId:
+ type:
+ - string
+ - 'null'
+ description: >-
+ The challenge handle returned by the login start, required for `SMS_OTP`
+ and omitted for other factors.
+ code:
+ type:
+ - string
+ - 'null'
+ description: >-
+ The one-time code the customer received by SMS, or read from their
+ authenticator app. Provide for `SMS_OTP` / `TOTP`. In sandbox, the code is
+ always `123456`.
+ example: '123456'
+ passkeyAssertion:
+ type:
+ - object
+ - 'null'
+ additionalProperties: true
+ description: >-
+ Opaque WebAuthn assertion produced by the device from the login start's
+ `passkeyOptions`. Required when completing a `PASSKEY` login.
+ origin:
+ type:
+ - string
+ - 'null'
+ description: >-
+ The WebAuthn origin the `passkeyAssertion` was produced against (one of
+ the login start's `allowedOrigins`). Required alongside `passkeyAssertion`;
+ omit it for the `code` path.
+ example: https://app.example.com
diff --git a/openapi/components/schemas/sca/ScaLoginStart.yaml b/openapi/components/schemas/sca/ScaLoginStart.yaml
new file mode 100644
index 000000000..b4d71b14d
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaLoginStart.yaml
@@ -0,0 +1,55 @@
+type: object
+description: >-
+ The factor-specific material a customer needs to complete an SCA login. Each
+ factor surfaces only the fields it issues: `SMS_OTP` carries `challengeId` and
+ `expiresAt`; `TOTP` carries neither (the customer reads the code from their
+ authenticator app); `PASSKEY` carries the opaque WebAuthn `passkeyOptions`
+ with `allowedOrigins` and `relyingPartyId`.
+required:
+ - factor
+properties:
+ factor:
+ $ref: ./ScaFactor.yaml
+ description: The factor this login was started for.
+ challengeId:
+ type:
+ - string
+ - 'null'
+ description: >-
+ The challenge handle for an `SMS_OTP` login, threaded back on the complete
+ call. Present only for `SMS_OTP`.
+ expiresAt:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: >-
+ Absolute UTC timestamp after which the `SMS_OTP` code expires. Present
+ only for `SMS_OTP`.
+ example: '2025-10-03T12:05:00Z'
+ passkeyOptions:
+ type:
+ - object
+ - 'null'
+ additionalProperties: true
+ description: >-
+ Opaque WebAuthn assertion request options. Present only for `PASSKEY`;
+ pass to the device's WebAuthn API to produce the assertion submitted on
+ the complete call.
+ allowedOrigins:
+ type:
+ - array
+ - 'null'
+ items:
+ type: string
+ description: >-
+ The origins the WebAuthn ceremony may run against. Present only for
+ `PASSKEY`.
+ example:
+ - https://app.example.com
+ relyingPartyId:
+ type:
+ - string
+ - 'null'
+ description: The WebAuthn relying-party id. Present only for `PASSKEY`.
+ example: app.example.com
diff --git a/openapi/components/schemas/sca/ScaLoginStartRequest.yaml b/openapi/components/schemas/sca/ScaLoginStartRequest.yaml
new file mode 100644
index 000000000..9974b26db
--- /dev/null
+++ b/openapi/components/schemas/sca/ScaLoginStartRequest.yaml
@@ -0,0 +1,10 @@
+type: object
+description: >-
+ Selects which enrolled factor to start an SCA login with. The factor must
+ already be enrolled (or, for `SMS_OTP`, the phone verified).
+required:
+ - factor
+properties:
+ factor:
+ $ref: ./ScaFactor.yaml
+ description: The factor to authenticate with.
diff --git a/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml b/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml
new file mode 100644
index 000000000..df6bc1a81
--- /dev/null
+++ b/openapi/components/schemas/sca/TotpEnrollmentConfirmRequest.yaml
@@ -0,0 +1,26 @@
+type: object
+description: >-
+ The shared secret returned by the TOTP enrollment start, plus the first code
+ the customer's authenticator app produces, submitted to confirm and finalize
+ the TOTP factor.
+required:
+ - type
+ - secret
+ - code
+properties:
+ type:
+ type: string
+ enum:
+ - TOTP
+ description: Discriminator selecting the TOTP confirm variant.
+ secret:
+ type: string
+ description: >-
+ The shared secret returned as `secret` by the TOTP enrollment start,
+ threaded back to bind the confirmation to that enrollment.
+ code:
+ type: string
+ description: >-
+ The current time-based one-time code from the customer's authenticator
+ app. In sandbox, the code is always `123456`.
+ example: '123456'
diff --git a/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml b/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml
new file mode 100644
index 000000000..b3d7e9aab
--- /dev/null
+++ b/openapi/components/schemas/sca/TotpEnrollmentConfirmResponse.yaml
@@ -0,0 +1,22 @@
+type: object
+description: >-
+ The one-time recovery codes issued once a TOTP factor is enrolled. These are
+ shown to the customer only once; store them somewhere safe to recover access
+ if the authenticator device is lost.
+required:
+ - type
+ - recoveryCodes
+properties:
+ type:
+ type: string
+ enum:
+ - TOTP
+ description: Discriminator identifying this as the TOTP enrollment result.
+ recoveryCodes:
+ type: array
+ description: The one-time recovery codes for this TOTP factor.
+ items:
+ type: string
+ example:
+ - ABCD-EFGH-IJKL
+ - MNOP-QRST-UVWX
diff --git a/openapi/components/schemas/sca/TotpEnrollmentStart.yaml b/openapi/components/schemas/sca/TotpEnrollmentStart.yaml
new file mode 100644
index 000000000..26dffef54
--- /dev/null
+++ b/openapi/components/schemas/sca/TotpEnrollmentStart.yaml
@@ -0,0 +1,31 @@
+type: object
+description: >-
+ The shared secret a customer's authenticator app needs to enroll a TOTP
+ factor. Returned by `POST /sca/factors` for a `TOTP` request; the
+ customer scans `totpUri` (an `otpauth://` provisioning URI) and confirms with
+ the first code their app produces.
+required:
+ - type
+ - secret
+ - secretBase32Encoded
+ - totpUri
+properties:
+ type:
+ type: string
+ enum:
+ - TOTP
+ description: Discriminator identifying this as the TOTP enrollment-start payload.
+ secret:
+ type: string
+ description: The raw TOTP shared secret.
+ secretBase32Encoded:
+ type: string
+ description: >-
+ The Base32-encoded shared secret, suitable for manual entry into an
+ authenticator app that does not scan QR codes.
+ totpUri:
+ type: string
+ description: >-
+ The `otpauth://` provisioning URI (the QR-code payload) the customer's
+ authenticator app scans to enroll the factor.
+ example: otpauth://totp/Grid:customer@example.com?secret=ABC123&issuer=Grid
diff --git a/openapi/components/schemas/sca/TotpFactorEnrollRequest.yaml b/openapi/components/schemas/sca/TotpFactorEnrollRequest.yaml
new file mode 100644
index 000000000..80db0c56d
--- /dev/null
+++ b/openapi/components/schemas/sca/TotpFactorEnrollRequest.yaml
@@ -0,0 +1,11 @@
+type: object
+title: TOTP Factor Enroll Request
+description: Start enrolling a time-based one-time-password (TOTP) authenticator factor.
+required:
+ - type
+properties:
+ type:
+ type: string
+ enum:
+ - TOTP
+ description: Discriminator selecting the TOTP factor. TOTP enrollment needs no other input at start.
diff --git a/openapi/components/schemas/sca/TwoFactorResetCompleteRequest.yaml b/openapi/components/schemas/sca/TwoFactorResetCompleteRequest.yaml
new file mode 100644
index 000000000..46df49fba
--- /dev/null
+++ b/openapi/components/schemas/sca/TwoFactorResetCompleteRequest.yaml
@@ -0,0 +1,23 @@
+type: object
+description: >-
+ Optional body for completing a 2FA reset. Only needed when resetting the
+ `SMS_OTP` factor to a new phone number; omit the body entirely otherwise.
+properties:
+ mobile:
+ type: object
+ description: >-
+ For an `SMS_OTP` reset, the new mobile number to enroll as the customer
+ completes the reset. Required for an `SMS_OTP` reset; ignored for `TOTP` /
+ `PASSKEY` resets.
+ required:
+ - countryCode
+ - number
+ properties:
+ countryCode:
+ type: string
+ description: The country dialing code, including the leading `+`.
+ example: '+1'
+ number:
+ type: string
+ description: The national subscriber number, without the country code.
+ example: '4155550123'
diff --git a/openapi/components/schemas/sca/TwoFactorResetStart.yaml b/openapi/components/schemas/sca/TwoFactorResetStart.yaml
new file mode 100644
index 000000000..e0183c819
--- /dev/null
+++ b/openapi/components/schemas/sca/TwoFactorResetStart.yaml
@@ -0,0 +1,35 @@
+type: object
+description: >-
+ The reset handle plus the opaque liveness handles a caller relays to
+ the end-user device to complete the liveness check. `resetId` threads the
+ ceremony together (status and complete reference it). `livenessAccessToken`
+ and `verificationLink` are omitted when they are not returned.
+required:
+ - resetId
+properties:
+ resetId:
+ type: string
+ description: >-
+ Identifier for this reset; pass it to the status and complete endpoints.
+ livenessAccessToken:
+ type:
+ - string
+ - 'null'
+ description: >-
+ Access token for the embedded liveness/verification SDK,
+ bound to this reset. Omitted when one is not returned.
+ verificationLink:
+ type:
+ - string
+ - 'null'
+ description: >-
+ Hosted identity-verification page URL for completing liveness. Omitted
+ when one is not returned.
+ expiresAt:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: >-
+ Absolute UTC timestamp at the end of the reset window. Omitted when one is not returned.
+ example: '2025-10-03T12:30:00Z'
diff --git a/openapi/components/schemas/sca/TwoFactorResetStartRequest.yaml b/openapi/components/schemas/sca/TwoFactorResetStartRequest.yaml
new file mode 100644
index 000000000..2af4a6daf
--- /dev/null
+++ b/openapi/components/schemas/sca/TwoFactorResetStartRequest.yaml
@@ -0,0 +1,8 @@
+type: object
+description: Selects which enrolled factor to reset via the liveness-gated recovery flow.
+required:
+ - factor
+properties:
+ factor:
+ $ref: ./ScaFactor.yaml
+ description: The enrolled factor to reset.
diff --git a/openapi/components/schemas/sca/TwoFactorResetStatus.yaml b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml
new file mode 100644
index 000000000..c26598b62
--- /dev/null
+++ b/openapi/components/schemas/sca/TwoFactorResetStatus.yaml
@@ -0,0 +1,59 @@
+type: object
+description: The status of an in-progress 2FA reset, polled until it reaches a terminal value.
+required:
+ - status
+ - factor
+ - expiresAt
+properties:
+ status:
+ type: string
+ enum:
+ - INITIATED
+ - PENDING_REVIEW
+ - LIVENESS_PASSED
+ - COMPLETED
+ - REJECTED
+ - EXPIRED
+ description: >-
+ The reset status.
+
+ | Value | Terminal | Meaning |
+ |-------|----------|---------|
+ | `INITIATED` | no | Reset started; liveness not yet submitted. Keep polling. |
+ | `PENDING_REVIEW` | no | Liveness submitted; under review. Keep polling. |
+ | `LIVENESS_PASSED` | no | Liveness passed; call the complete endpoint to finish the reset. |
+ | `COMPLETED` | **yes** | Reset finished; the lost factor is cleared and re-enrollment can begin. |
+ | `REJECTED` | **yes** | Liveness failed. Stop polling; start a new reset. |
+ | `EXPIRED` | **yes** | The reset window closed before it completed. Stop polling; start a new reset. |
+
+ Stop polling on any terminal value.
+ example: INITIATED
+ factor:
+ $ref: ./ScaFactor.yaml
+ description: The factor being reset.
+ enrollmentStatus:
+ type:
+ - string
+ - 'null'
+ enum:
+ - PENDING
+ - COMPLETED
+ - null
+ description: >-
+ The enrollment status of the replacement factor. `PENDING` until the
+ customer finishes re-enrolling, then `COMPLETED`. Null for an `SMS_OTP`
+ reset, where completing the reset enrolls the new number directly with no
+ separate enrollment step.
+ example: PENDING
+ expiresAt:
+ type: string
+ format: date-time
+ description: When the reset window closes. Poll no longer than this.
+ example: '2025-10-03T12:15:00Z'
+ completedAt:
+ type:
+ - string
+ - 'null'
+ format: date-time
+ description: When the reset completed; null until then.
+ example: null
diff --git a/openapi/components/schemas/transfers/TransferOutRequest.yaml b/openapi/components/schemas/transfers/TransferOutRequest.yaml
index 865360b34..1678b4070 100644
--- a/openapi/components/schemas/transfers/TransferOutRequest.yaml
+++ b/openapi/components/schemas/transfers/TransferOutRequest.yaml
@@ -26,11 +26,3 @@ properties:
remittanceInformation field, and for wires it populates the OBI
(Originator to Beneficiary Information) / beneficiary information.
example: '12345'
- scaFactor:
- $ref: ../sca/ScaFactor.yaml
- description: >-
- Optional preferred factor for the Strong Customer Authentication challenge
- this call issues. Only relevant for customers in a region where SCA is required
- (e.g. EU); ignored otherwise. Valid values for a per-transaction challenge
- are `SMS_OTP` (default) and `PASSKEY` — `TOTP` cannot carry the required
- dynamic linking and is rejected here. Omit to default to `SMS_OTP`.
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index a4865267f..7254a8e98 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -153,6 +153,32 @@ paths:
$ref: paths/customers/customers_{customerId}_verify-phone.yaml
/customers/{customerId}/verify-phone/confirm:
$ref: paths/customers/customers_{customerId}_verify-phone_confirm.yaml
+ /sca/factors:
+ $ref: paths/sca/sca_factors.yaml
+ /sca/factors/confirm:
+ $ref: paths/sca/sca_factors_confirm.yaml
+ /sca/factors/{credentialId}:
+ $ref: paths/sca/sca_factors_{credentialId}.yaml
+ /sca/login/start:
+ $ref: paths/sca/sca_login_start.yaml
+ /sca/login/complete:
+ $ref: paths/sca/sca_login_complete.yaml
+ /sca/record-event:
+ $ref: paths/sca/sca_record-event.yaml
+ /sca/factors/reset:
+ $ref: paths/sca/sca_factors_reset.yaml
+ /sca/factors/reset/{resetId}:
+ $ref: paths/sca/sca_factors_reset_{resetId}.yaml
+ /sca/factors/reset/{resetId}/complete:
+ $ref: paths/sca/sca_factors_reset_{resetId}_complete.yaml
+ /customers/external-accounts/{externalAccountId}/trust:
+ $ref: paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml
+ /customers/external-accounts/{externalAccountId}/trust/confirm:
+ $ref: paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml
+ /customers/external-accounts/{externalAccountId}/untrust:
+ $ref: paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml
+ /customers/external-accounts/{externalAccountId}/untrust/confirm:
+ $ref: paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml
/customers/internal-accounts:
$ref: paths/customers/customers_internal_accounts.yaml
/platform/internal-accounts:
diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml
new file mode 100644
index 000000000..52853b191
--- /dev/null
+++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust.yaml
@@ -0,0 +1,58 @@
+parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being trusted.
+ required: true
+ schema:
+ type: string
+post:
+ summary: Start trusting a beneficiary
+ description: |
+ Begin trusting (whitelisting) an external account so future sends to it can
+ skip the per-transaction SCA ceremony. Returns the `scaChallenge` to satisfy
+ (when one is issued). Complete with
+ `POST /customers/external-accounts/{externalAccountId}/trust/confirm`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startBeneficiaryTrust
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Beneficiary trust started; the SCA challenge (if any) is returned.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/BeneficiaryTrustStart.yaml
+ '400':
+ description: Invalid request
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer or external account not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml
new file mode 100644
index 000000000..d69d2e888
--- /dev/null
+++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_trust_confirm.yaml
@@ -0,0 +1,66 @@
+parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being trusted.
+ required: true
+ schema:
+ type: string
+post:
+ summary: Confirm trusting a beneficiary
+ description: |
+ Finalize trusting a beneficiary (identified by the `externalAccountId` in the
+ path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or
+ `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when
+ one was issued. Returns `trusted: true`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS/TOTP code is always `123456`.
+ operationId: confirmBeneficiaryTrust
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml
+ responses:
+ '200':
+ description: Beneficiary trusted.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/BeneficiaryTrustConfirm.yaml
+ '400':
+ description: Invalid or expired proof
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer or external account not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml
new file mode 100644
index 000000000..85e17b1e3
--- /dev/null
+++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust.yaml
@@ -0,0 +1,59 @@
+parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being untrusted.
+ required: true
+ schema:
+ type: string
+post:
+ summary: Start untrusting a beneficiary
+ description: |
+ Begin untrusting (removing the trusted mark from) an external account, so
+ future sends to it are dynamically linked and require the per-transaction SCA
+ ceremony again. Returns the `scaChallenge` to satisfy (when one is issued).
+ Complete with
+ `POST /customers/external-accounts/{externalAccountId}/untrust/confirm`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startBeneficiaryUntrust
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Beneficiary untrust started; the SCA challenge (if any) is returned.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/BeneficiaryTrustStart.yaml
+ '400':
+ description: Invalid request
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer or external account not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml
new file mode 100644
index 000000000..ec5d38a64
--- /dev/null
+++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_untrust_confirm.yaml
@@ -0,0 +1,66 @@
+parameters:
+ - name: externalAccountId
+ in: path
+ description: The unique identifier of the external account (beneficiary) being untrusted.
+ required: true
+ schema:
+ type: string
+post:
+ summary: Confirm untrusting a beneficiary
+ description: |
+ Finalize untrusting a beneficiary (identified by the `externalAccountId` in
+ the path) by submitting the SCA proof (`code` for `SMS_OTP` / `TOTP`, or
+ `passkeyAssertion` + `origin` for `PASSKEY`), echoing the `challengeId` when
+ one was issued. Returns `trusted: false`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS/TOTP code is always `123456`.
+ operationId: confirmBeneficiaryUntrust
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/BeneficiaryTrustConfirmRequest.yaml
+ responses:
+ '200':
+ description: Beneficiary untrusted.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/BeneficiaryTrustConfirm.yaml
+ '400':
+ description: Invalid or expired proof
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer or external account not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sca/sca_factors.yaml b/openapi/paths/sca/sca_factors.yaml
new file mode 100644
index 000000000..5d6da61a6
--- /dev/null
+++ b/openapi/paths/sca/sca_factors.yaml
@@ -0,0 +1,121 @@
+parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer whose factors are listed or enrolled.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+get:
+ summary: List enrolled SCA factors
+ description: |
+ List the Strong Customer Authentication factors the customer has enrolled.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: listScaFactors
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: The customer's enrolled SCA factors.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/ScaFactorList.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
+post:
+ summary: Start SCA factor enrollment
+ description: |
+ Begin enrolling an SCA factor for the customer. Enrollment covers the
+ explicit, opt-in factors a customer chooses to add — the request body's
+ `type` selects `TOTP` or `PASSKEY`. Returns the factor-specific material
+ needed to finish via `POST /sca/factors/confirm`.
+
+ `SMS_OTP` is implicit and is not enrolled through this endpoint. Every
+ customer in an SCA-regulated region has a verified phone number from
+ customer creation (via the Contact Verification flows —
+ `POST /customers/{customerId}/verify-phone` and `.../verify-phone/confirm`),
+ so SMS is always available as a factor with no extra setup and appears
+ among the customer's enrolled factors in `GET /sca/factors`.
+
+ A customer may have **only one passkey**. Starting a passkey enrollment when
+ one is already enrolled returns `409` (`PASSKEY_ALREADY_ENROLLED`) — delete it
+ via `DELETE /sca/factors/{credentialId}` first.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startScaFactorEnrollment
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/ScaFactorEnrollRequestOneOf.yaml
+ responses:
+ '200':
+ description: Enrollment started; the factor-specific completion material is returned.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/ScaFactorEnrollStartOneOf.yaml
+ '400':
+ description: Invalid request
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: >-
+ SCA is not required for this customer (`CONFLICT`), or a passkey
+ enrollment was requested while one is already enrolled
+ (`PASSKEY_ALREADY_ENROLLED`) — only one passkey per customer is supported.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sca/sca_factors_confirm.yaml b/openapi/paths/sca/sca_factors_confirm.yaml
new file mode 100644
index 000000000..370cc219f
--- /dev/null
+++ b/openapi/paths/sca/sca_factors_confirm.yaml
@@ -0,0 +1,72 @@
+parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer confirming a factor enrollment.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+post:
+ summary: Confirm SCA factor enrollment
+ description: |
+ Finalize the factor enrollment started by `POST /sca/factors`. The request
+ body is discriminated by `type`: for `TOTP`, submit the shared `secret` from
+ the start call plus the first `code`; for `PASSKEY`, submit the WebAuthn
+ `credential` the device produced plus the `origin` it was produced against.
+ The threaded secret/credential binds the confirmation to its enrollment, so
+ no separate id is needed.
+
+ A TOTP confirmation returns one-time recovery codes (shown once); a passkey
+ confirmation returns the enrolled factor.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the TOTP code is always `123456`.
+ operationId: confirmScaFactorEnrollment
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/ScaFactorConfirmRequestOneOf.yaml
+ responses:
+ '200':
+ description: Factor enrolled; the factor-specific result is returned.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/ScaFactorConfirmResponseOneOf.yaml
+ '400':
+ description: Invalid or incorrect confirmation proof
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sca/sca_factors_reset.yaml b/openapi/paths/sca/sca_factors_reset.yaml
new file mode 100644
index 000000000..d7fa24870
--- /dev/null
+++ b/openapi/paths/sca/sca_factors_reset.yaml
@@ -0,0 +1,75 @@
+parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer resetting a factor.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+post:
+ summary: Start a 2FA reset
+ description: |
+ Begin recovering a lost enrolled factor via a liveness-gated, poll-based
+ flow. Opens the liveness check and returns a `resetId` plus the
+ opaque liveness handles (`livenessAccessToken` / `verificationLink`) the end
+ user completes it with. Poll
+ `GET /sca/factors/reset/{resetId}` until liveness
+ passes, then call the complete endpoint.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startTwoFactorReset
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/TwoFactorResetStartRequest.yaml
+ responses:
+ '201':
+ description: Reset initiated; the reset handle and liveness material are returned.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/TwoFactorResetStart.yaml
+ '400':
+ description: Invalid or unknown factor
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '429':
+ description: >-
+ Too many reset attempts. Reset initiation is rate-limited to 5 per 24
+ hours per customer; retry after the window indicated by `Retry-After`.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error429.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sca/sca_factors_reset_{resetId}.yaml b/openapi/paths/sca/sca_factors_reset_{resetId}.yaml
new file mode 100644
index 000000000..dcf436481
--- /dev/null
+++ b/openapi/paths/sca/sca_factors_reset_{resetId}.yaml
@@ -0,0 +1,56 @@
+parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer whose reset status is polled.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ - name: resetId
+ in: path
+ description: The reset handle returned by the start call.
+ required: true
+ schema:
+ type: string
+get:
+ summary: Get 2FA reset status
+ description: |
+ Poll the status of an in-progress 2FA reset until it reaches the liveness-passed value, after which the reset can be completed.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: getTwoFactorResetStatus
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: The current reset status.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/TwoFactorResetStatus.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer or reset not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sca/sca_factors_reset_{resetId}_complete.yaml b/openapi/paths/sca/sca_factors_reset_{resetId}_complete.yaml
new file mode 100644
index 000000000..2d454df13
--- /dev/null
+++ b/openapi/paths/sca/sca_factors_reset_{resetId}_complete.yaml
@@ -0,0 +1,68 @@
+parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer completing the reset.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ - name: resetId
+ in: path
+ description: The reset handle returned by the start call.
+ required: true
+ schema:
+ type: string
+post:
+ summary: Complete a 2FA reset
+ description: |
+ Complete a 2FA reset once liveness has passed, clearing the lost factor so
+ the customer can re-enroll.
+
+ For an `SMS_OTP` reset, supply the new `mobile` number in the body — completing
+ the reset enrolls it as the customer's number. Other factors need no body.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: completeTwoFactorReset
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/TwoFactorResetCompleteRequest.yaml
+ responses:
+ '204':
+ description: Reset completed; no content is returned.
+ '400':
+ description: Reset not ready (liveness not yet passed)
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer or reset not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sca/sca_factors_{credentialId}.yaml b/openapi/paths/sca/sca_factors_{credentialId}.yaml
new file mode 100644
index 000000000..4e651d44b
--- /dev/null
+++ b/openapi/paths/sca/sca_factors_{credentialId}.yaml
@@ -0,0 +1,53 @@
+parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer whose factor is being deleted.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+ - name: credentialId
+ in: path
+ description: The credential id of the enrolled factor to delete (from the factor's `credentialId`).
+ required: true
+ schema:
+ type: string
+delete:
+ summary: Delete an enrolled SCA factor
+ description: |
+ Delete an enrolled SCA factor by its credential id. Today only `PASSKEY`
+ factors carry a `credentialId` and are deletable this way.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: deleteScaFactor
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ responses:
+ '204':
+ description: Factor deleted; no content is returned.
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer or factor not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sca/sca_login_complete.yaml b/openapi/paths/sca/sca_login_complete.yaml
new file mode 100644
index 000000000..8f85304f2
--- /dev/null
+++ b/openapi/paths/sca/sca_login_complete.yaml
@@ -0,0 +1,75 @@
+parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer completing an SCA login.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+post:
+ summary: Complete an SCA login
+ description: |
+ Finalize an SCA login by submitting the proof for the started factor
+ (`code` for `SMS_OTP` / `TOTP`, or `passkeyAssertion` + `origin` for
+ `PASSKEY`), echoing the `challengeId` for `SMS_OTP`. Returns the
+ reported session status.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+
+ In sandbox, the SMS/TOTP code is always `123456`.
+ operationId: completeScaLogin
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/ScaLoginCompleteRequest.yaml
+ responses:
+ '200':
+ description: SCA login completed; the session status is returned.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/ScaLoginComplete.yaml
+ '400':
+ description: Invalid or expired proof
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '423':
+ description: >-
+ The customer's login is locked (or suspended) after too many failed
+ attempts. `details.lockedUntil` says when they may retry.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error423.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sca/sca_login_start.yaml b/openapi/paths/sca/sca_login_start.yaml
new file mode 100644
index 000000000..1329c138c
--- /dev/null
+++ b/openapi/paths/sca/sca_login_start.yaml
@@ -0,0 +1,68 @@
+parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer starting an SCA login.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+post:
+ summary: Start an SCA login
+ description: |
+ Begin an SCA login for the customer with the chosen factor, opening the
+ end-user SCA session (an exemption gating read / account access beyond the
+ per-transaction window). Returns factor-specific material: `SMS_OTP`
+ dispatches a code and returns a `challengeId` + `expiresAt`; `TOTP` returns
+ only the factor (the customer reads the code from their app); `PASSKEY`
+ returns WebAuthn `passkeyOptions`. Complete with
+ `POST /sca/login/complete`.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: startScaLogin
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/ScaLoginStartRequest.yaml
+ responses:
+ '200':
+ description: SCA login started; factor-specific material is returned.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/ScaLoginStart.yaml
+ '400':
+ description: Invalid or unknown factor
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/sca/sca_record-event.yaml b/openapi/paths/sca/sca_record-event.yaml
new file mode 100644
index 000000000..a2dbbabd0
--- /dev/null
+++ b/openapi/paths/sca/sca_record-event.yaml
@@ -0,0 +1,73 @@
+parameters:
+ - name: customerId
+ in: query
+ description: The unique identifier of the customer the security event is recorded for.
+ required: true
+ schema:
+ type: string
+ example: Customer:019542f5-b3e7-1d02-0000-000000000001
+post:
+ summary: Record a security event
+ description: |
+ Record a client-side security-relevant event for the customer with Grid's risk engine (e.g. a sign-in, a sensitive view), to
+ feed adaptive-authentication signals.
+
+ This endpoint is only meaningful for customers in a region where SCA is required (e.g. EU). For customers outside SCA-regulated regions, this returns `409`.
+ operationId: recordSecurityEvent
+ tags:
+ - Strong Customer Authentication
+ security:
+ - BasicAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/RecordSecurityEventRequest.yaml
+ responses:
+ '200':
+ description: >-
+ Event recorded; the customer's resulting login-security state is
+ returned (including any lockout).
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/sca/RecordSecurityEventResponse.yaml
+ '400':
+ description: Invalid event type
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error400.yaml
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error401.yaml
+ '404':
+ description: Customer not found
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error404.yaml
+ '409':
+ description: SCA is not required for this customer.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error409.yaml
+ '423':
+ description: >-
+ The customer's login is locked (or suspended) after too many failed
+ attempts. `details.lockedUntil` says when they may retry.
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error423.yaml
+ '500':
+ description: Internal service error
+ content:
+ application/json:
+ schema:
+ $ref: ../../components/schemas/errors/Error500.yaml
diff --git a/openapi/paths/transfers/transfer_out.yaml b/openapi/paths/transfers/transfer_out.yaml
index 85831695f..47a032663 100644
--- a/openapi/paths/transfers/transfer_out.yaml
+++ b/openapi/paths/transfers/transfer_out.yaml
@@ -38,17 +38,6 @@ post:
'201':
description: >
Transfer-out request created successfully.
-
-
- For customers outside SCA-regulated regions the transaction proceeds as
- usual.
-
-
- **Strong Customer Authentication (EU):** per-transaction SCA is authorized
- only on the quote resource (`POST /quotes/{quoteId}/authorize`), and
- `transfer-out` has no associated quote. `transfer-out` is being
- deprecated, so SCA-gated EU debits are not offered on this endpoint — use
- the quote + `execute` flow instead.
content:
application/json:
schema: