From a50b908e1b9f67cecc94a9ae4ea418aeff8b0ea8 Mon Sep 17 00:00:00 2001 From: shreyav Date: Wed, 5 Aug 2026 21:54:22 -0700 Subject: [PATCH 01/11] feat: add external account ownership verification Add verify-ownership and verify-ownership/confirm endpoints for customer and platform external accounts, supporting wallet-signature and liveness verification methods. Adds an ownershipVerificationStatus field to ExternalAccount, EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_* webhook types, new error codes, and an expanded ownershipType description. Co-Authored-By: Claude Fable 5 --- mintlify/openapi.yaml | 492 +++++++++++++++++- openapi.yaml | 492 +++++++++++++++++- .../components/schemas/errors/Error400.yaml | 8 + .../external_accounts/ExternalAccount.yaml | 3 + .../external_accounts/OwnershipType.yaml | 8 +- .../OwnershipVerificationConfirmRequest.yaml | 26 + .../OwnershipVerificationMethod.yaml | 12 + .../OwnershipVerificationStart.yaml | 32 ++ .../OwnershipVerificationStartRequest.yaml | 10 + .../OwnershipVerificationStatus.yaml | 17 + .../webhooks/ExternalAccountWebhook.yaml | 14 + .../schemas/webhooks/WebhookType.yaml | 3 + openapi/openapi.yaml | 10 + ..._{externalAccountId}_verify-ownership.yaml | 73 +++ ...alAccountId}_verify-ownership_confirm.yaml | 70 +++ ..._{externalAccountId}_verify-ownership.yaml | 73 +++ ...alAccountId}_verify-ownership_confirm.yaml | 70 +++ openapi/webhooks/external-account.yaml | 110 ++++ 18 files changed, 1519 insertions(+), 4 deletions(-) create mode 100644 openapi/components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml create mode 100644 openapi/components/schemas/external_accounts/OwnershipVerificationMethod.yaml create mode 100644 openapi/components/schemas/external_accounts/OwnershipVerificationStart.yaml create mode 100644 openapi/components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml create mode 100644 openapi/components/schemas/external_accounts/OwnershipVerificationStatus.yaml create mode 100644 openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml create mode 100644 openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml create mode 100644 openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml create mode 100644 openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml create mode 100644 openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml create mode 100644 openapi/webhooks/external-account.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 75833f86e..46d0c89d7 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2429,6 +2429,151 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/external-accounts/{externalAccountId}/verify-ownership: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string + post: + summary: Start external account ownership verification + description: | + Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet + external account. Choose a `method`: + + - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the + wallet sign it and submit the result to + `POST /customers/external-accounts/{externalAccountId}/verify-ownership/confirm` + to complete verification synchronously. + - `LIVENESS` — the response includes a `verificationLink` (and a `token` + for embedding); the user completes a hosted biometric flow and + verification completes asynchronously. Status transitions are delivered + via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in + the account's `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts whose + `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. + operationId: verifyExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationStartRequest' + responses: + '200': + description: Ownership verification started; the method-specific material is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Ownership verification is not applicable to this external account. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /customers/external-accounts/{externalAccountId}/verify-ownership/confirm: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string + post: + summary: Confirm external account ownership verification + description: | + Complete a `WALLET_SIGNATURE` ownership verification by submitting the + signature the wallet produced for the `messageToSign` returned by + `POST /customers/external-accounts/{externalAccountId}/verify-ownership`. + The message must be signed exactly as returned, and the signature must be + submitted before the session's `expiresAt`; after expiry, start a new + verification. + + Returns the updated external account, including its new + `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts with an in-progress + `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. + operationId: confirmExternalAccountOwnershipVerification + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' + responses: + '200': + description: Ownership verification submitted; the updated external account is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '400': + description: Invalid or expired signature + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Ownership verification is not applicable to this external account. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /platform/external-accounts: get: summary: List platform external accounts @@ -2633,6 +2778,151 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /platform/external-accounts/{externalAccountId}/verify-ownership: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string + post: + summary: Start platform external account ownership verification + description: | + Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet + external account owned by the platform. Choose a `method`: + + - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the + wallet sign it and submit the result to + `POST /platform/external-accounts/{externalAccountId}/verify-ownership/confirm` + to complete verification synchronously. + - `LIVENESS` — the response includes a `verificationLink` (and a `token` + for embedding); the user completes a hosted biometric flow and + verification completes asynchronously. Status transitions are delivered + via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in + the account's `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts whose + `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. + operationId: verifyPlatformExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationStartRequest' + responses: + '200': + description: Ownership verification started; the method-specific material is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: External account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Ownership verification is not applicable to this external account. + 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}/verify-ownership/confirm: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string + post: + summary: Confirm platform external account ownership verification + description: | + Complete a `WALLET_SIGNATURE` ownership verification by submitting the + signature the wallet produced for the `messageToSign` returned by + `POST /platform/external-accounts/{externalAccountId}/verify-ownership`. + The message must be signed exactly as returned, and the signature must be + submitted before the session's `expiresAt`; after expiry, start a new + verification. + + Returns the updated external account, including its new + `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts with an in-progress + `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. + operationId: confirmPlatformExternalAccountOwnershipVerification + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' + responses: + '200': + description: Ownership verification submitted; the updated external account is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '400': + description: Invalid or expired signature + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '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' + '409': + description: Ownership verification is not applicable to this external account. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /beneficial-owners: post: summary: Create a beneficial owner @@ -10750,6 +11040,93 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + external-account: + post: + summary: External account ownership verification status change + description: | + Webhook that is called when the ownership verification status of an external account 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. + + ### Event types + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full external account object. + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full external account object. + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED` — Fired when an ownership verification attempt fails; a new verification can be started. The `data` payload contains the full external account object. + operationId: externalAccountWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccountWebhook' + examples: + ownershipVerified: + summary: Ownership of a self-custody wallet has been verified + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000040 + type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED + timestamp: '2025-08-15T14:32:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: ACTIVE + currency: USDC + ownershipType: FIRST_PARTY + ownershipVerificationStatus: VERIFIED + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + ownershipVerificationFailed: + summary: An ownership verification attempt failed + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000041 + type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: ACTIVE + currency: USDC + ownershipType: FIRST_PARTY + ownershipVerificationStatus: FAILED + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + 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' verification-update: post: summary: Verification status change @@ -11838,6 +12215,10 @@ components: | STABLECOIN_PROVIDER_ACCOUNT_INVALID | The stablecoin provider account link is not usable | | STABLECOIN_PROVIDER_ACCOUNT_REVOKED | The stablecoin provider account link has been revoked | | STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED | Multiple active provider account links exist; pass `stablecoinProviderAccountId` to select one | + | OWNERSHIP_TYPE_REQUIRED | `ownershipType` must be provided for this external account | + | WALLET_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | + | THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for third-party accounts | + | LIGHTNING_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for this destination type | enum: - INVALID_INPUT - END_USER_TERMS_VERSION_NOT_FOUND @@ -11879,6 +12260,10 @@ components: - STABLECOIN_PROVIDER_ACCOUNT_INVALID - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED + - OWNERSHIP_TYPE_REQUIRED + - WALLET_VERIFICATION_REQUIRED + - THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED + - LIGHTNING_TRANSFER_LIMIT_EXCEEDED message: type: string description: Error message @@ -16848,7 +17233,7 @@ components: enum: - FIRST_PARTY - THIRD_PARTY - description: Whether the external account belongs to the customer themselves (first party) or to someone else (third party) + description: Whether the external account belongs to the customer themselves (`FIRST_PARTY`) or to someone else (`THIRD_PARTY`). Required when creating self-custody crypto wallet external accounts on platforms subject to EU Travel Rule requirements; recommended for all other accounts, where providing it can unlock additional capabilities and smoother compliance handling. example: FIRST_PARTY BeneficiaryVerificationStatus: type: string @@ -16877,6 +17262,24 @@ components: type: string description: The verified full name of the account holder as returned by the payment rail example: John Doe + OwnershipVerificationStatus: + type: string + enum: + - NOT_REQUIRED + - REQUIRED + - PENDING_REVIEW + - FAILED + - VERIFIED + description: | + The status of ownership verification for this external account. + + | Status | Description | + |--------|-------------| + | `NOT_REQUIRED` | Ownership verification does not apply to this account | + | `REQUIRED` | Ownership must be verified before transfers above regulatory thresholds can be sent to this account | + | `PENDING_REVIEW` | A verification was submitted and is under review | + | `FAILED` | The most recent verification attempt failed; a new verification can be started | + | `VERIFIED` | Ownership has been verified; no further action is needed | ExternalAccountType: type: string enum: @@ -19236,6 +19639,9 @@ components: beneficiaryVerifiedData: $ref: '#/components/schemas/BeneficiaryVerifiedData' description: Verified beneficiary data returned by the payment rail, if available + ownershipVerificationStatus: + $ref: '#/components/schemas/OwnershipVerificationStatus' + description: The status of ownership verification for this account accountInfo: $ref: '#/components/schemas/ExternalAccountInfoOneOf' ExternalAccountListResponse: @@ -20142,6 +20548,72 @@ components: default: false accountInfo: $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' + OwnershipVerificationMethod: + type: string + enum: + - WALLET_SIGNATURE + - LIVENESS + description: | + The method used to verify ownership of a self-custody crypto wallet. + + | Method | Description | + |--------|-------------| + | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | + | `LIVENESS` | Prove identity via a hosted biometric verification flow | + example: WALLET_SIGNATURE + OwnershipVerificationStartRequest: + type: object + description: Starts ownership verification for a self-custody crypto wallet external account. + required: + - method + properties: + method: + $ref: '#/components/schemas/OwnershipVerificationMethod' + description: The verification method to use. + OwnershipVerificationStart: + type: object + description: 'The material needed to complete an ownership verification. Which fields are populated depends on the requested `method`: `messageToSign` for `WALLET_SIGNATURE`; `verificationLink` and `token` for `LIVENESS`.' + required: + - expiresAt + properties: + messageToSign: + type: string + description: '`WALLET_SIGNATURE` only. The exact message the wallet must sign, character-for-character.' + example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + verificationLink: + type: string + format: uri + description: '`LIVENESS` only. Hosted verification URL to present to the user.' + example: https://verify.example.com/session/019542f5-b3e7-1d02 + token: + type: string + description: '`LIVENESS` only. Access token for embedding the verification flow in the platform''s own UI, as an alternative to `verificationLink`.' + expiresAt: + type: string + format: date-time + description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. + example: '2025-08-15T15:32:00Z' + OwnershipVerificationConfirmRequest: + type: object + description: Completes a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the `messageToSign` from the start step. + required: + - signature + - signedAddress + properties: + signature: + type: string + description: The signature produced over the exact `messageToSign` — EIP-191 hex for EVM chains, base64 for Bitcoin, base58-encoded Ed25519 for Solana. + signedAddress: + type: string + description: The wallet address that signed the message. + example: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + signatureScheme: + type: string + enum: + - bip137 + - electrum + default: bip137 + description: Bitcoin message-signing format. Defaults to `bip137`; use `electrum` for Electrum/Sparrow wallets. Ignored for non-Bitcoin chains. PlatformExternalAccountCreateRequest: type: object required: @@ -24692,6 +25164,9 @@ components: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED - INVITATION.CLAIMED @@ -24886,6 +25361,21 @@ components: enum: - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED + ExternalAccountWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ExternalAccount' + type: + type: string + enum: + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED VerificationWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' diff --git a/openapi.yaml b/openapi.yaml index 75833f86e..46d0c89d7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2429,6 +2429,151 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/external-accounts/{externalAccountId}/verify-ownership: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string + post: + summary: Start external account ownership verification + description: | + Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet + external account. Choose a `method`: + + - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the + wallet sign it and submit the result to + `POST /customers/external-accounts/{externalAccountId}/verify-ownership/confirm` + to complete verification synchronously. + - `LIVENESS` — the response includes a `verificationLink` (and a `token` + for embedding); the user completes a hosted biometric flow and + verification completes asynchronously. Status transitions are delivered + via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in + the account's `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts whose + `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. + operationId: verifyExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationStartRequest' + responses: + '200': + description: Ownership verification started; the method-specific material is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Ownership verification is not applicable to this external account. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /customers/external-accounts/{externalAccountId}/verify-ownership/confirm: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string + post: + summary: Confirm external account ownership verification + description: | + Complete a `WALLET_SIGNATURE` ownership verification by submitting the + signature the wallet produced for the `messageToSign` returned by + `POST /customers/external-accounts/{externalAccountId}/verify-ownership`. + The message must be signed exactly as returned, and the signature must be + submitted before the session's `expiresAt`; after expiry, start a new + verification. + + Returns the updated external account, including its new + `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts with an in-progress + `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. + operationId: confirmExternalAccountOwnershipVerification + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' + responses: + '200': + description: Ownership verification submitted; the updated external account is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '400': + description: Invalid or expired signature + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Customer or external account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Ownership verification is not applicable to this external account. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /platform/external-accounts: get: summary: List platform external accounts @@ -2633,6 +2778,151 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /platform/external-accounts/{externalAccountId}/verify-ownership: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string + post: + summary: Start platform external account ownership verification + description: | + Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet + external account owned by the platform. Choose a `method`: + + - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the + wallet sign it and submit the result to + `POST /platform/external-accounts/{externalAccountId}/verify-ownership/confirm` + to complete verification synchronously. + - `LIVENESS` — the response includes a `verificationLink` (and a `token` + for embedding); the user completes a hosted biometric flow and + verification completes asynchronously. Status transitions are delivered + via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in + the account's `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts whose + `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. + operationId: verifyPlatformExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationStartRequest' + responses: + '200': + description: Ownership verification started; the method-specific material is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationStart' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: External account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Ownership verification is not applicable to this external account. + 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}/verify-ownership/confirm: + parameters: + - name: externalAccountId + in: path + description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string + post: + summary: Confirm platform external account ownership verification + description: | + Complete a `WALLET_SIGNATURE` ownership verification by submitting the + signature the wallet produced for the `messageToSign` returned by + `POST /platform/external-accounts/{externalAccountId}/verify-ownership`. + The message must be signed exactly as returned, and the signature must be + submitted before the session's `expiresAt`; after expiry, start a new + verification. + + Returns the updated external account, including its new + `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts with an in-progress + `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. + operationId: confirmPlatformExternalAccountOwnershipVerification + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' + responses: + '200': + description: Ownership verification submitted; the updated external account is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '400': + description: Invalid or expired signature + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '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' + '409': + description: Ownership verification is not applicable to this external account. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /beneficial-owners: post: summary: Create a beneficial owner @@ -10750,6 +11040,93 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + external-account: + post: + summary: External account ownership verification status change + description: | + Webhook that is called when the ownership verification status of an external account 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. + + ### Event types + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full external account object. + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full external account object. + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED` — Fired when an ownership verification attempt fails; a new verification can be started. The `data` payload contains the full external account object. + operationId: externalAccountWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccountWebhook' + examples: + ownershipVerified: + summary: Ownership of a self-custody wallet has been verified + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000040 + type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED + timestamp: '2025-08-15T14:32:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: ACTIVE + currency: USDC + ownershipType: FIRST_PARTY + ownershipVerificationStatus: VERIFIED + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + ownershipVerificationFailed: + summary: An ownership verification attempt failed + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000041 + type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: ACTIVE + currency: USDC + ownershipType: FIRST_PARTY + ownershipVerificationStatus: FAILED + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + 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' verification-update: post: summary: Verification status change @@ -11838,6 +12215,10 @@ components: | STABLECOIN_PROVIDER_ACCOUNT_INVALID | The stablecoin provider account link is not usable | | STABLECOIN_PROVIDER_ACCOUNT_REVOKED | The stablecoin provider account link has been revoked | | STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED | Multiple active provider account links exist; pass `stablecoinProviderAccountId` to select one | + | OWNERSHIP_TYPE_REQUIRED | `ownershipType` must be provided for this external account | + | WALLET_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | + | THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for third-party accounts | + | LIGHTNING_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for this destination type | enum: - INVALID_INPUT - END_USER_TERMS_VERSION_NOT_FOUND @@ -11879,6 +12260,10 @@ components: - STABLECOIN_PROVIDER_ACCOUNT_INVALID - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED + - OWNERSHIP_TYPE_REQUIRED + - WALLET_VERIFICATION_REQUIRED + - THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED + - LIGHTNING_TRANSFER_LIMIT_EXCEEDED message: type: string description: Error message @@ -16848,7 +17233,7 @@ components: enum: - FIRST_PARTY - THIRD_PARTY - description: Whether the external account belongs to the customer themselves (first party) or to someone else (third party) + description: Whether the external account belongs to the customer themselves (`FIRST_PARTY`) or to someone else (`THIRD_PARTY`). Required when creating self-custody crypto wallet external accounts on platforms subject to EU Travel Rule requirements; recommended for all other accounts, where providing it can unlock additional capabilities and smoother compliance handling. example: FIRST_PARTY BeneficiaryVerificationStatus: type: string @@ -16877,6 +17262,24 @@ components: type: string description: The verified full name of the account holder as returned by the payment rail example: John Doe + OwnershipVerificationStatus: + type: string + enum: + - NOT_REQUIRED + - REQUIRED + - PENDING_REVIEW + - FAILED + - VERIFIED + description: | + The status of ownership verification for this external account. + + | Status | Description | + |--------|-------------| + | `NOT_REQUIRED` | Ownership verification does not apply to this account | + | `REQUIRED` | Ownership must be verified before transfers above regulatory thresholds can be sent to this account | + | `PENDING_REVIEW` | A verification was submitted and is under review | + | `FAILED` | The most recent verification attempt failed; a new verification can be started | + | `VERIFIED` | Ownership has been verified; no further action is needed | ExternalAccountType: type: string enum: @@ -19236,6 +19639,9 @@ components: beneficiaryVerifiedData: $ref: '#/components/schemas/BeneficiaryVerifiedData' description: Verified beneficiary data returned by the payment rail, if available + ownershipVerificationStatus: + $ref: '#/components/schemas/OwnershipVerificationStatus' + description: The status of ownership verification for this account accountInfo: $ref: '#/components/schemas/ExternalAccountInfoOneOf' ExternalAccountListResponse: @@ -20142,6 +20548,72 @@ components: default: false accountInfo: $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' + OwnershipVerificationMethod: + type: string + enum: + - WALLET_SIGNATURE + - LIVENESS + description: | + The method used to verify ownership of a self-custody crypto wallet. + + | Method | Description | + |--------|-------------| + | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | + | `LIVENESS` | Prove identity via a hosted biometric verification flow | + example: WALLET_SIGNATURE + OwnershipVerificationStartRequest: + type: object + description: Starts ownership verification for a self-custody crypto wallet external account. + required: + - method + properties: + method: + $ref: '#/components/schemas/OwnershipVerificationMethod' + description: The verification method to use. + OwnershipVerificationStart: + type: object + description: 'The material needed to complete an ownership verification. Which fields are populated depends on the requested `method`: `messageToSign` for `WALLET_SIGNATURE`; `verificationLink` and `token` for `LIVENESS`.' + required: + - expiresAt + properties: + messageToSign: + type: string + description: '`WALLET_SIGNATURE` only. The exact message the wallet must sign, character-for-character.' + example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + verificationLink: + type: string + format: uri + description: '`LIVENESS` only. Hosted verification URL to present to the user.' + example: https://verify.example.com/session/019542f5-b3e7-1d02 + token: + type: string + description: '`LIVENESS` only. Access token for embedding the verification flow in the platform''s own UI, as an alternative to `verificationLink`.' + expiresAt: + type: string + format: date-time + description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. + example: '2025-08-15T15:32:00Z' + OwnershipVerificationConfirmRequest: + type: object + description: Completes a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the `messageToSign` from the start step. + required: + - signature + - signedAddress + properties: + signature: + type: string + description: The signature produced over the exact `messageToSign` — EIP-191 hex for EVM chains, base64 for Bitcoin, base58-encoded Ed25519 for Solana. + signedAddress: + type: string + description: The wallet address that signed the message. + example: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + signatureScheme: + type: string + enum: + - bip137 + - electrum + default: bip137 + description: Bitcoin message-signing format. Defaults to `bip137`; use `electrum` for Electrum/Sparrow wallets. Ignored for non-Bitcoin chains. PlatformExternalAccountCreateRequest: type: object required: @@ -24692,6 +25164,9 @@ components: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED - INVITATION.CLAIMED @@ -24886,6 +25361,21 @@ components: enum: - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED + ExternalAccountWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ExternalAccount' + type: + type: string + enum: + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED VerificationWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' diff --git a/openapi/components/schemas/errors/Error400.yaml b/openapi/components/schemas/errors/Error400.yaml index e38f562c2..dc073dccd 100644 --- a/openapi/components/schemas/errors/Error400.yaml +++ b/openapi/components/schemas/errors/Error400.yaml @@ -54,6 +54,10 @@ properties: | STABLECOIN_PROVIDER_ACCOUNT_INVALID | The stablecoin provider account link is not usable | | STABLECOIN_PROVIDER_ACCOUNT_REVOKED | The stablecoin provider account link has been revoked | | STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED | Multiple active provider account links exist; pass `stablecoinProviderAccountId` to select one | + | OWNERSHIP_TYPE_REQUIRED | `ownershipType` must be provided for this external account | + | WALLET_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | + | THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for third-party accounts | + | LIGHTNING_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for this destination type | enum: - INVALID_INPUT - END_USER_TERMS_VERSION_NOT_FOUND @@ -95,6 +99,10 @@ properties: - STABLECOIN_PROVIDER_ACCOUNT_INVALID - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED + - OWNERSHIP_TYPE_REQUIRED + - WALLET_VERIFICATION_REQUIRED + - THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED + - LIGHTNING_TRANSFER_LIMIT_EXCEEDED message: type: string description: Error message diff --git a/openapi/components/schemas/external_accounts/ExternalAccount.yaml b/openapi/components/schemas/external_accounts/ExternalAccount.yaml index 08449b833..aa2099cdb 100644 --- a/openapi/components/schemas/external_accounts/ExternalAccount.yaml +++ b/openapi/components/schemas/external_accounts/ExternalAccount.yaml @@ -42,5 +42,8 @@ allOf: beneficiaryVerifiedData: $ref: ./BeneficiaryVerifiedData.yaml description: Verified beneficiary data returned by the payment rail, if available + ownershipVerificationStatus: + $ref: ./OwnershipVerificationStatus.yaml + description: The status of ownership verification for this account accountInfo: $ref: ./ExternalAccountInfoOneOf.yaml diff --git a/openapi/components/schemas/external_accounts/OwnershipType.yaml b/openapi/components/schemas/external_accounts/OwnershipType.yaml index e2b6bce03..6f369994e 100644 --- a/openapi/components/schemas/external_accounts/OwnershipType.yaml +++ b/openapi/components/schemas/external_accounts/OwnershipType.yaml @@ -3,6 +3,10 @@ enum: - FIRST_PARTY - THIRD_PARTY description: >- - Whether the external account belongs to the customer themselves (first party) - or to someone else (third party) + Whether the external account belongs to the customer themselves + (`FIRST_PARTY`) or to someone else (`THIRD_PARTY`). Required when creating + self-custody crypto wallet external accounts on platforms subject to EU + Travel Rule requirements; recommended for all other accounts, where + providing it can unlock additional capabilities and smoother compliance + handling. example: FIRST_PARTY diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml b/openapi/components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml new file mode 100644 index 000000000..25c675387 --- /dev/null +++ b/openapi/components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml @@ -0,0 +1,26 @@ +type: object +description: >- + Completes a `WALLET_SIGNATURE` ownership verification by submitting the + signature the wallet produced for the `messageToSign` from the start step. +required: + - signature + - signedAddress +properties: + signature: + type: string + description: >- + The signature produced over the exact `messageToSign` — EIP-191 hex for + EVM chains, base64 for Bitcoin, base58-encoded Ed25519 for Solana. + signedAddress: + type: string + description: The wallet address that signed the message. + example: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + signatureScheme: + type: string + enum: + - bip137 + - electrum + default: bip137 + description: >- + Bitcoin message-signing format. Defaults to `bip137`; use `electrum` for + Electrum/Sparrow wallets. Ignored for non-Bitcoin chains. diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationMethod.yaml b/openapi/components/schemas/external_accounts/OwnershipVerificationMethod.yaml new file mode 100644 index 000000000..6211b5928 --- /dev/null +++ b/openapi/components/schemas/external_accounts/OwnershipVerificationMethod.yaml @@ -0,0 +1,12 @@ +type: string +enum: + - WALLET_SIGNATURE + - LIVENESS +description: | + The method used to verify ownership of a self-custody crypto wallet. + + | Method | Description | + |--------|-------------| + | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | + | `LIVENESS` | Prove identity via a hosted biometric verification flow | +example: WALLET_SIGNATURE diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationStart.yaml b/openapi/components/schemas/external_accounts/OwnershipVerificationStart.yaml new file mode 100644 index 000000000..2987e0b97 --- /dev/null +++ b/openapi/components/schemas/external_accounts/OwnershipVerificationStart.yaml @@ -0,0 +1,32 @@ +type: object +description: >- + The material needed to complete an ownership verification. Which fields are + populated depends on the requested `method`: `messageToSign` for + `WALLET_SIGNATURE`; `verificationLink` and `token` for `LIVENESS`. +required: + - expiresAt +properties: + messageToSign: + type: string + description: >- + `WALLET_SIGNATURE` only. The exact message the wallet must sign, + character-for-character. + example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + verificationLink: + type: string + format: uri + description: >- + `LIVENESS` only. Hosted verification URL to present to the user. + example: https://verify.example.com/session/019542f5-b3e7-1d02 + token: + type: string + description: >- + `LIVENESS` only. Access token for embedding the verification flow in the + platform's own UI, as an alternative to `verificationLink`. + expiresAt: + type: string + format: date-time + description: >- + When this verification session expires. Prompt the user promptly; after + expiry, a new verification must be started. + example: '2025-08-15T15:32:00Z' diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml b/openapi/components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml new file mode 100644 index 000000000..447fc9da4 --- /dev/null +++ b/openapi/components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml @@ -0,0 +1,10 @@ +type: object +description: >- + Starts ownership verification for a self-custody crypto wallet external + account. +required: + - method +properties: + method: + $ref: ./OwnershipVerificationMethod.yaml + description: The verification method to use. diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationStatus.yaml b/openapi/components/schemas/external_accounts/OwnershipVerificationStatus.yaml new file mode 100644 index 000000000..199c3ffa0 --- /dev/null +++ b/openapi/components/schemas/external_accounts/OwnershipVerificationStatus.yaml @@ -0,0 +1,17 @@ +type: string +enum: + - NOT_REQUIRED + - REQUIRED + - PENDING_REVIEW + - FAILED + - VERIFIED +description: | + The status of ownership verification for this external account. + + | Status | Description | + |--------|-------------| + | `NOT_REQUIRED` | Ownership verification does not apply to this account | + | `REQUIRED` | Ownership must be verified before transfers above regulatory thresholds can be sent to this account | + | `PENDING_REVIEW` | A verification was submitted and is under review | + | `FAILED` | The most recent verification attempt failed; a new verification can be started | + | `VERIFIED` | Ownership has been verified; no further action is needed | diff --git a/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml b/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml new file mode 100644 index 000000000..23006152b --- /dev/null +++ b/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml @@ -0,0 +1,14 @@ +allOf: + - $ref: ./BaseWebhook.yaml + - type: object + required: + - data + properties: + data: + $ref: ../external_accounts/ExternalAccount.yaml + type: + type: string + enum: + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml index fab72c524..b6ec84ded 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -26,6 +26,9 @@ enum: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED - INVITATION.CLAIMED diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index b6143351f..db1c9dd0d 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -189,10 +189,18 @@ paths: $ref: paths/customers/customers_external_accounts.yaml /customers/external-accounts/{externalAccountId}: $ref: paths/customers/customers_external_accounts_{externalAccountId}.yaml + /customers/external-accounts/{externalAccountId}/verify-ownership: + $ref: paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml + /customers/external-accounts/{externalAccountId}/verify-ownership/confirm: + $ref: paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml /platform/external-accounts: $ref: paths/platform/platform_external_accounts.yaml /platform/external-accounts/{externalAccountId}: $ref: paths/platform/platform_external_accounts_{externalAccountId}.yaml + /platform/external-accounts/{externalAccountId}/verify-ownership: + $ref: paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml + /platform/external-accounts/{externalAccountId}/verify-ownership/confirm: + $ref: paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml /beneficial-owners: $ref: paths/beneficial-owners/beneficial_owners.yaml /beneficial-owners/{beneficialOwnerId}: @@ -389,6 +397,8 @@ webhooks: $ref: webhooks/customer-update.yaml internal-account-status: $ref: webhooks/internal-account-status.yaml + external-account: + $ref: webhooks/external-account.yaml verification-update: $ref: webhooks/verification-update.yaml card-state-change: diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml new file mode 100644 index 000000000..2a9dcceec --- /dev/null +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml @@ -0,0 +1,73 @@ +parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string +post: + summary: Start external account ownership verification + description: | + Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet + external account. Choose a `method`: + + - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the + wallet sign it and submit the result to + `POST /customers/external-accounts/{externalAccountId}/verify-ownership/confirm` + to complete verification synchronously. + - `LIVENESS` — the response includes a `verificationLink` (and a `token` + for embedding); the user completes a hosted biometric flow and + verification completes asynchronously. Status transitions are delivered + via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in + the account's `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts whose + `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. + operationId: verifyExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml + responses: + '200': + description: Ownership verification started; the method-specific material is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipVerificationStart.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: Ownership verification is not applicable to this external account. + 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}_verify-ownership_confirm.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml new file mode 100644 index 000000000..e5895f3f3 --- /dev/null +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml @@ -0,0 +1,70 @@ +parameters: + - name: externalAccountId + in: path + description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string +post: + summary: Confirm external account ownership verification + description: | + Complete a `WALLET_SIGNATURE` ownership verification by submitting the + signature the wallet produced for the `messageToSign` returned by + `POST /customers/external-accounts/{externalAccountId}/verify-ownership`. + The message must be signed exactly as returned, and the signature must be + submitted before the session's `expiresAt`; after expiry, start a new + verification. + + Returns the updated external account, including its new + `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts with an in-progress + `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. + operationId: confirmExternalAccountOwnershipVerification + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml + responses: + '200': + description: Ownership verification submitted; the updated external account is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/ExternalAccount.yaml + '400': + description: Invalid or expired signature + 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: Ownership verification is not applicable to this external account. + 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/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml new file mode 100644 index 000000000..993e275dc --- /dev/null +++ b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml @@ -0,0 +1,73 @@ +parameters: + - name: externalAccountId + in: path + description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string +post: + summary: Start platform external account ownership verification + description: | + Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet + external account owned by the platform. Choose a `method`: + + - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the + wallet sign it and submit the result to + `POST /platform/external-accounts/{externalAccountId}/verify-ownership/confirm` + to complete verification synchronously. + - `LIVENESS` — the response includes a `verificationLink` (and a `token` + for embedding); the user completes a hosted biometric flow and + verification completes asynchronously. Status transitions are delivered + via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in + the account's `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts whose + `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. + operationId: verifyPlatformExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml + responses: + '200': + description: Ownership verification started; the method-specific material is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipVerificationStart.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: External account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: Ownership verification is not applicable to this external account. + 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/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml new file mode 100644 index 000000000..48b04e11f --- /dev/null +++ b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml @@ -0,0 +1,70 @@ +parameters: + - name: externalAccountId + in: path + description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. + required: true + schema: + type: string +post: + summary: Confirm platform external account ownership verification + description: | + Complete a `WALLET_SIGNATURE` ownership verification by submitting the + signature the wallet produced for the `messageToSign` returned by + `POST /platform/external-accounts/{externalAccountId}/verify-ownership`. + The message must be signed exactly as returned, and the signature must be + submitted before the session's `expiresAt`; after expiry, start a new + verification. + + Returns the updated external account, including its new + `ownershipVerificationStatus`. + + This endpoint is only meaningful for accounts with an in-progress + `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. + operationId: confirmPlatformExternalAccountOwnershipVerification + tags: + - External Accounts + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml + responses: + '200': + description: Ownership verification submitted; the updated external account is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/ExternalAccount.yaml + '400': + description: Invalid or expired signature + 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: External account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: Ownership verification is not applicable to this external account. + 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/webhooks/external-account.yaml b/openapi/webhooks/external-account.yaml new file mode 100644 index 000000000..c969e5cf6 --- /dev/null +++ b/openapi/webhooks/external-account.yaml @@ -0,0 +1,110 @@ +post: + summary: External account ownership verification status change + description: > + Webhook that is called when the ownership verification status of an + external account 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. + + + ### Event types + + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW` — Fired when a + submitted ownership verification enters review. The `data` payload contains + the full external account object. + + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED` — Fired when ownership + of the external account has been verified. The `data` payload contains the + full external account object. + + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED` — Fired when an + ownership verification attempt fails; a new verification can be started. + The `data` payload contains the full external account object. + + + operationId: externalAccountWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/webhooks/ExternalAccountWebhook.yaml + examples: + ownershipVerified: + summary: Ownership of a self-custody wallet has been verified + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000040 + type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED + timestamp: '2025-08-15T14:32:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: ACTIVE + currency: USDC + ownershipType: FIRST_PARTY + ownershipVerificationStatus: VERIFIED + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + ownershipVerificationFailed: + summary: An ownership verification attempt failed + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000041 + type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: ACTIVE + currency: USDC + ownershipType: FIRST_PARTY + ownershipVerificationStatus: FAILED + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + responses: + '200': + description: > + Webhook received successfully + '400': + description: Bad request + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized - Signature validation failed + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error401.yaml + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error409.yaml From e2029e17f1d5693b1906fc5dbf8a90a01f0b4b98 Mon Sep 17 00:00:00 2001 From: shreyav Date: Wed, 5 Aug 2026 22:01:21 -0700 Subject: [PATCH 02/11] Add creation-time ownership-verification webhook types for lifecycle consistency Payments fire webhooks for their creation-time state (OUTGOING_PAYMENT.PENDING), so ownership verification does the same: NOT_REQUIRED and REQUIRED join the async transitions, giving webhook consumers the full lifecycle. Co-Authored-By: Claude Fable 5 --- mintlify/openapi.yaml | 6 ++++++ openapi.yaml | 6 ++++++ .../schemas/webhooks/ExternalAccountWebhook.yaml | 2 ++ openapi/components/schemas/webhooks/WebhookType.yaml | 2 ++ openapi/webhooks/external-account.yaml | 9 +++++++++ 5 files changed, 25 insertions(+) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 46d0c89d7..14a893762 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -11058,6 +11058,8 @@ webhooks: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. ### Event types + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED` — Fired when an external account is created and ownership verification does not apply to it. The `data` payload contains the full external account object. + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED` — Fired when an external account requires ownership verification before transfers above regulatory thresholds can be sent to it. The `data` payload contains the full external account object. - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full external account object. - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full external account object. - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED` — Fired when an ownership verification attempt fails; a new verification can be started. The `data` payload contains the full external account object. @@ -25164,6 +25166,8 @@ components: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED @@ -25373,6 +25377,8 @@ components: type: type: string enum: + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED diff --git a/openapi.yaml b/openapi.yaml index 46d0c89d7..14a893762 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -11058,6 +11058,8 @@ webhooks: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. ### Event types + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED` — Fired when an external account is created and ownership verification does not apply to it. The `data` payload contains the full external account object. + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED` — Fired when an external account requires ownership verification before transfers above regulatory thresholds can be sent to it. The `data` payload contains the full external account object. - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full external account object. - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full external account object. - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED` — Fired when an ownership verification attempt fails; a new verification can be started. The `data` payload contains the full external account object. @@ -25164,6 +25166,8 @@ components: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED @@ -25373,6 +25377,8 @@ components: type: type: string enum: + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED diff --git a/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml b/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml index 23006152b..136428934 100644 --- a/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml +++ b/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml @@ -9,6 +9,8 @@ allOf: type: type: string enum: + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml index b6ec84ded..80ad2f95d 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -26,6 +26,8 @@ enum: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED + - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED diff --git a/openapi/webhooks/external-account.yaml b/openapi/webhooks/external-account.yaml index c969e5cf6..510251415 100644 --- a/openapi/webhooks/external-account.yaml +++ b/openapi/webhooks/external-account.yaml @@ -29,6 +29,15 @@ post: ### Event types + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED` — Fired when an + external account is created and ownership verification does not apply to + it. The `data` payload contains the full external account object. + + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED` — Fired when an + external account requires ownership verification before transfers above + regulatory thresholds can be sent to it. The `data` payload contains the + full external account object. + - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full external account object. From 7bb52cc17949c2858777d3413152f0a925ecb1a9 Mon Sep 17 00:00:00 2001 From: shreyav Date: Wed, 5 Aug 2026 22:44:46 -0700 Subject: [PATCH 03/11] Rework ownership verification into a first-class /ownership-verifications resource Replace the external-account verb endpoints (verify-ownership + verify-ownership/confirm under /customers and /platform) with a top-level Ownership Verifications API mirroring the KYC/KYB Verifications pattern: - POST /ownership-verifications, GET /ownership-verifications (filter by externalAccountId/status with cursor pagination), GET /ownership-verifications/{verificationId}, and POST /ownership-verifications/{verificationId}/confirm - OwnershipVerification is a oneOf on method: WalletSignatureOwnershipVerification (messageToSign, confirmed synchronously) and LivenessOwnershipVerification (verificationLink + token, completes asynchronously) - Drop ExternalAccount.ownershipVerificationStatus; add the PENDING_OWNERSHIP_VERIFICATION external-account status for FIRST_PARTY self-custody wallets on platforms subject to EU Travel Rule requirements - Replace EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_* webhooks with OWNERSHIP_VERIFICATION.{PENDING_REVIEW,VERIFIED,FAILED} resource webhooks and a new EXTERNAL_ACCOUNT.STATUS_UPDATED webhook - Trim removed error codes and extend AMOUNT_OUT_OF_RANGE description Co-Authored-By: Claude Fable 5 --- mintlify/openapi.yaml | 747 +++++++++++------- openapi.yaml | 747 +++++++++++------- .../components/schemas/errors/Error400.yaml | 8 +- .../external_accounts/ExternalAccount.yaml | 3 - .../ExternalAccountStatus.yaml | 11 +- .../OwnershipVerificationStart.yaml | 32 - .../OwnershipVerificationStartRequest.yaml | 10 - .../OwnershipVerificationStatus.yaml | 17 - .../LivenessOwnershipVerification.yaml | 59 ++ .../OwnershipVerification.yaml | 11 + .../OwnershipVerificationConfirmRequest.yaml | 3 +- .../OwnershipVerificationListResponse.yaml | 21 + .../OwnershipVerificationMethod.yaml | 0 .../OwnershipVerificationRequest.yaml | 17 + .../OwnershipVerificationState.yaml | 16 + .../WalletSignatureOwnershipVerification.yaml | 51 ++ .../ExternalAccountStatusWebhook.yaml | 12 + .../webhooks/ExternalAccountWebhook.yaml | 16 - .../OwnershipVerificationWebhook.yaml | 14 + .../schemas/webhooks/WebhookType.yaml | 9 +- openapi/openapi.yaml | 24 +- ..._{externalAccountId}_verify-ownership.yaml | 73 -- ...alAccountId}_verify-ownership_confirm.yaml | 70 -- .../ownership-verifications.yaml | 133 ++++ ...ership-verifications_{verificationId}.yaml | 40 + ...erifications_{verificationId}_confirm.yaml | 72 ++ ..._{externalAccountId}_verify-ownership.yaml | 73 -- ...alAccountId}_verify-ownership_confirm.yaml | 70 -- openapi/webhooks/external-account-status.yaml | 86 ++ openapi/webhooks/external-account.yaml | 119 --- openapi/webhooks/ownership-verification.yaml | 109 +++ 31 files changed, 1562 insertions(+), 1111 deletions(-) delete mode 100644 openapi/components/schemas/external_accounts/OwnershipVerificationStart.yaml delete mode 100644 openapi/components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml delete mode 100644 openapi/components/schemas/external_accounts/OwnershipVerificationStatus.yaml create mode 100644 openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml create mode 100644 openapi/components/schemas/ownership_verifications/OwnershipVerification.yaml rename openapi/components/schemas/{external_accounts => ownership_verifications}/OwnershipVerificationConfirmRequest.yaml (83%) create mode 100644 openapi/components/schemas/ownership_verifications/OwnershipVerificationListResponse.yaml rename openapi/components/schemas/{external_accounts => ownership_verifications}/OwnershipVerificationMethod.yaml (100%) create mode 100644 openapi/components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml create mode 100644 openapi/components/schemas/ownership_verifications/OwnershipVerificationState.yaml create mode 100644 openapi/components/schemas/ownership_verifications/WalletSignatureOwnershipVerification.yaml create mode 100644 openapi/components/schemas/webhooks/ExternalAccountStatusWebhook.yaml delete mode 100644 openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml create mode 100644 openapi/components/schemas/webhooks/OwnershipVerificationWebhook.yaml delete mode 100644 openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml delete mode 100644 openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml create mode 100644 openapi/paths/ownership_verifications/ownership-verifications.yaml create mode 100644 openapi/paths/ownership_verifications/ownership-verifications_{verificationId}.yaml create mode 100644 openapi/paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml delete mode 100644 openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml delete mode 100644 openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml create mode 100644 openapi/webhooks/external-account-status.yaml delete mode 100644 openapi/webhooks/external-account.yaml create mode 100644 openapi/webhooks/ownership-verification.yaml diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 14a893762..d8ba938a3 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -33,6 +33,8 @@ tags: description: Internal account management endpoints for creating and managing internal accounts - name: External Accounts description: External account management endpoints for creating and managing external bank accounts + - name: Ownership Verifications + description: Endpoints for verifying ownership of self-custody crypto wallet external accounts, via wallet signature or a hosted biometric verification flow. - name: Same-Currency Transfers description: Endpoints for transferring funds between internal and external accounts with the same currency - name: Cross-Currency Transfers @@ -2429,151 +2431,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/external-accounts/{externalAccountId}/verify-ownership: - parameters: - - name: externalAccountId - in: path - description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string - post: - summary: Start external account ownership verification - description: | - Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet - external account. Choose a `method`: - - - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the - wallet sign it and submit the result to - `POST /customers/external-accounts/{externalAccountId}/verify-ownership/confirm` - to complete verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` (and a `token` - for embedding); the user completes a hosted biometric flow and - verification completes asynchronously. Status transitions are delivered - via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in - the account's `ownershipVerificationStatus`. - - This endpoint is only meaningful for accounts whose - `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. - operationId: verifyExternalAccountOwnership - tags: - - External Accounts - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerificationStartRequest' - responses: - '200': - description: Ownership verification started; the method-specific material is returned. - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerificationStart' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Customer or external account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: Ownership verification is not applicable to this external account. - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /customers/external-accounts/{externalAccountId}/verify-ownership/confirm: - parameters: - - name: externalAccountId - in: path - description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string - post: - summary: Confirm external account ownership verification - description: | - Complete a `WALLET_SIGNATURE` ownership verification by submitting the - signature the wallet produced for the `messageToSign` returned by - `POST /customers/external-accounts/{externalAccountId}/verify-ownership`. - The message must be signed exactly as returned, and the signature must be - submitted before the session's `expiresAt`; after expiry, start a new - verification. - - Returns the updated external account, including its new - `ownershipVerificationStatus`. - - This endpoint is only meaningful for accounts with an in-progress - `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. - operationId: confirmExternalAccountOwnershipVerification - tags: - - External Accounts - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' - responses: - '200': - description: Ownership verification submitted; the updated external account is returned. - content: - application/json: - schema: - $ref: '#/components/schemas/ExternalAccount' - '400': - description: Invalid or expired signature - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Customer or external account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: Ownership verification is not applicable to this external account. - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' /platform/external-accounts: get: summary: List platform external accounts @@ -2778,35 +2635,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts/{externalAccountId}/verify-ownership: - parameters: - - name: externalAccountId - in: path - description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string + /ownership-verifications: post: - summary: Start platform external account ownership verification + summary: Create an ownership verification description: | - Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet - external account owned by the platform. Choose a `method`: + Begin ownership verification for a `FIRST_PARTY` self-custody crypto + wallet external account (customer or platform owned). Choose a `method`: - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the wallet sign it and submit the result to - `POST /platform/external-accounts/{externalAccountId}/verify-ownership/confirm` - to complete verification synchronously. + `POST /ownership-verifications/{verificationId}/confirm` to complete + verification synchronously. - `LIVENESS` — the response includes a `verificationLink` (and a `token` for embedding); the user completes a hosted biometric flow and verification completes asynchronously. Status transitions are delivered - via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in - the account's `ownershipVerificationStatus`. + via `OWNERSHIP_VERIFICATION.*` webhooks or by polling + `GET /ownership-verifications/{verificationId}`. - This endpoint is only meaningful for accounts whose - `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. - operationId: verifyPlatformExternalAccountOwnership + Ownership verification applies to accounts in + `PENDING_OWNERSHIP_VERIFICATION` status; completing it moves the account + to `ACTIVE`. For accounts where ownership verification is not applicable, + this returns `409`. + operationId: createOwnershipVerification tags: - - External Accounts + - Ownership Verifications security: - BasicAuth: [] requestBody: @@ -2814,16 +2666,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerificationStartRequest' + $ref: '#/components/schemas/OwnershipVerificationRequest' responses: - '200': - description: Ownership verification started; the method-specific material is returned. + '201': + description: Ownership verification created; the method-specific material is returned. content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerificationStart' + $ref: '#/components/schemas/OwnershipVerification' '400': - description: Invalid request + description: Bad request - Invalid parameters content: application/json: schema: @@ -2852,34 +2704,136 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts/{externalAccountId}/verify-ownership/confirm: - parameters: - - name: externalAccountId - in: path - description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string + get: + summary: List ownership verifications + description: | + Retrieve a list of ownership verifications with optional filtering by external account ID and status. + operationId: listOwnershipVerifications + tags: + - Ownership Verifications + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: query + description: Filter by external account ID + required: false + schema: + type: string + - name: status + in: query + description: Filter by verification status + required: false + schema: + $ref: '#/components/schemas/OwnershipVerificationState' + - 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/OwnershipVerificationListResponse' + '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' + /ownership-verifications/{verificationId}: + get: + summary: Get an ownership verification + description: Retrieve details of a specific ownership verification by ID. + operationId: getOwnershipVerification + tags: + - Ownership Verifications + security: + - BasicAuth: [] + parameters: + - name: verificationId + in: path + description: Ownership verification ID + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerification' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Ownership verification not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /ownership-verifications/{verificationId}/confirm: post: - summary: Confirm platform external account ownership verification + summary: Confirm an ownership verification description: | Complete a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the `messageToSign` returned by - `POST /platform/external-accounts/{externalAccountId}/verify-ownership`. - The message must be signed exactly as returned, and the signature must be - submitted before the session's `expiresAt`; after expiry, start a new - verification. - - Returns the updated external account, including its new - `ownershipVerificationStatus`. + `POST /ownership-verifications`. The message must be signed exactly as + returned, and the signature must be submitted before the session's + `expiresAt`; after expiry, start a new verification. - This endpoint is only meaningful for accounts with an in-progress - `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. - operationId: confirmPlatformExternalAccountOwnershipVerification + This endpoint is only valid for `WALLET_SIGNATURE` verifications in + `PENDING` status. For other verifications, this returns `409`. `LIVENESS` + verifications complete asynchronously — their status is delivered via + `OWNERSHIP_VERIFICATION.*` webhooks or by polling + `GET /ownership-verifications/{verificationId}`. + operationId: confirmOwnershipVerification tags: - - External Accounts + - Ownership Verifications security: - BasicAuth: [] + parameters: + - name: verificationId + in: path + description: Ownership verification ID + required: true + schema: + type: string requestBody: required: true content: @@ -2888,11 +2842,11 @@ paths: $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' responses: '200': - description: Ownership verification submitted; the updated external account is returned. + description: Signature submitted; the updated ownership verification is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/OwnershipVerification' '400': description: Invalid or expired signature content: @@ -2906,13 +2860,13 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Ownership verification not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: Ownership verification is not applicable to this external account. + description: The verification is not a `WALLET_SIGNATURE` verification in `PENDING` status. content: application/json: schema: @@ -11040,11 +10994,11 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' - external-account: + external-account-status: post: - summary: External account ownership verification status change + summary: External account status webhook description: | - Webhook that is called when the ownership verification status of an external account changes. + Webhook that is called when the status of an external account changes (e.g., `PENDING_OWNERSHIP_VERIFICATION` → `ACTIVE` after ownership verification completes). This endpoint should be implemented by clients of the Grid API. ### Authentication @@ -11058,12 +11012,8 @@ webhooks: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. ### Event types - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED` — Fired when an external account is created and ownership verification does not apply to it. The `data` payload contains the full external account object. - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED` — Fired when an external account requires ownership verification before transfers above regulatory thresholds can be sent to it. The `data` payload contains the full external account object. - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full external account object. - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full external account object. - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED` — Fired when an ownership verification attempt fails; a new verification can be started. The `data` payload contains the full external account object. - operationId: externalAccountWebhook + - `EXTERNAL_ACCOUNT.STATUS_UPDATED` — Fired when the status of an external account changes. The `data` payload contains the full external account object. + operationId: externalAccountStatusWebhook tags: - Webhooks security: @@ -11073,29 +11023,13 @@ webhooks: content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountWebhook' + $ref: '#/components/schemas/ExternalAccountStatusWebhook' examples: - ownershipVerified: - summary: Ownership of a self-custody wallet has been verified - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000040 - type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - timestamp: '2025-08-15T14:32:00Z' - data: - id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 - status: ACTIVE - currency: USDC - ownershipType: FIRST_PARTY - ownershipVerificationStatus: VERIFIED - accountInfo: - accountType: ETHEREUM_WALLET - address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' - ownershipVerificationFailed: - summary: An ownership verification attempt failed + statusUpdated: + summary: A wallet account became active after ownership verification value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000041 - type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + id: Webhook:019542f5-b3e7-1d02-0000-000000000042 + type: EXTERNAL_ACCOUNT.STATUS_UPDATED timestamp: '2025-08-15T14:32:00Z' data: id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 @@ -11103,7 +11037,6 @@ webhooks: status: ACTIVE currency: USDC ownershipType: FIRST_PARTY - ownershipVerificationStatus: FAILED accountInfo: accountType: ETHEREUM_WALLET address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' @@ -11208,6 +11141,92 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + ownership-verification: + post: + summary: Ownership verification status change + description: | + Webhook that is called when the status of an ownership verification 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. + + ### Event types + - `OWNERSHIP_VERIFICATION.PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full ownership verification object. + - `OWNERSHIP_VERIFICATION.VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full ownership verification object. + - `OWNERSHIP_VERIFICATION.FAILED` — Fired when an ownership verification attempt fails; start a new verification to retry. The `data` payload contains the full ownership verification object. + operationId: ownershipVerificationWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationWebhook' + examples: + verified: + summary: Ownership of a self-custody wallet has been verified + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000040 + type: OWNERSHIP_VERIFICATION.VERIFIED + timestamp: '2025-08-15T14:32:00Z' + data: + id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 + externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: WALLET_SIGNATURE + status: VERIFIED + messageToSign: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + expiresAt: '2025-08-15T15:32:00Z' + createdAt: '2025-08-15T15:02:00Z' + updatedAt: '2025-08-15T14:32:00Z' + failed: + summary: An ownership verification attempt failed + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000041 + type: OWNERSHIP_VERIFICATION.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 + externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: LIVENESS + status: FAILED + verificationLink: https://verify.example.com/session/019542f5-b3e7-1d02 + token: eyJhbGciOiJIUzI1NiJ9.example + expiresAt: '2025-08-15T15:32:00Z' + createdAt: '2025-08-15T15:02:00Z' + updatedAt: '2025-08-15T14:32:00Z' + 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' card-state-change: post: summary: Card state change @@ -12193,7 +12212,7 @@ components: | INVALID_PUBKEY_FORMAT | Counterparty Public key format is invalid | | MISSING_REQUIRED_UMA_PARAMETERS | Counterparty required UMA parameters are missing | | SENDER_NOT_ACCEPTED | Sender is not accepted | - | AMOUNT_OUT_OF_RANGE | Amount is out of range | + | AMOUNT_OUT_OF_RANGE | Amount is out of range for the quote or the destination account | | INVALID_CURRENCY | Currency is invalid | | INVALID_TIMESTAMP | Timestamp is invalid | | INVALID_NONCE | Nonce is invalid | @@ -12217,10 +12236,7 @@ components: | STABLECOIN_PROVIDER_ACCOUNT_INVALID | The stablecoin provider account link is not usable | | STABLECOIN_PROVIDER_ACCOUNT_REVOKED | The stablecoin provider account link has been revoked | | STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED | Multiple active provider account links exist; pass `stablecoinProviderAccountId` to select one | - | OWNERSHIP_TYPE_REQUIRED | `ownershipType` must be provided for this external account | | WALLET_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | - | THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for third-party accounts | - | LIGHTNING_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for this destination type | enum: - INVALID_INPUT - END_USER_TERMS_VERSION_NOT_FOUND @@ -12262,10 +12278,7 @@ components: - STABLECOIN_PROVIDER_ACCOUNT_INVALID - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED - - OWNERSHIP_TYPE_REQUIRED - WALLET_VERIFICATION_REQUIRED - - THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED - - LIGHTNING_TRANSFER_LIMIT_EXCEEDED message: type: string description: Error message @@ -17227,9 +17240,18 @@ components: enum: - PENDING - ACTIVE + - PENDING_OWNERSHIP_VERIFICATION - UNDER_REVIEW - INACTIVE - description: Status of an external account + description: | + Status of an external account. + + `PENDING_OWNERSHIP_VERIFICATION` applies to `FIRST_PARTY` self-custody + crypto wallet accounts on platforms subject to EU Travel Rule requirements. + While in this status, the account can be used for transfers below regulatory + thresholds; completing ownership verification (see the Ownership + Verifications API) moves the account to `ACTIVE` and removes the + restriction. OwnershipType: type: string enum: @@ -17264,24 +17286,6 @@ components: type: string description: The verified full name of the account holder as returned by the payment rail example: John Doe - OwnershipVerificationStatus: - type: string - enum: - - NOT_REQUIRED - - REQUIRED - - PENDING_REVIEW - - FAILED - - VERIFIED - description: | - The status of ownership verification for this external account. - - | Status | Description | - |--------|-------------| - | `NOT_REQUIRED` | Ownership verification does not apply to this account | - | `REQUIRED` | Ownership must be verified before transfers above regulatory thresholds can be sent to this account | - | `PENDING_REVIEW` | A verification was submitted and is under review | - | `FAILED` | The most recent verification attempt failed; a new verification can be started | - | `VERIFIED` | Ownership has been verified; no further action is needed | ExternalAccountType: type: string enum: @@ -19641,9 +19645,6 @@ components: beneficiaryVerifiedData: $ref: '#/components/schemas/BeneficiaryVerifiedData' description: Verified beneficiary data returned by the payment rail, if available - ownershipVerificationStatus: - $ref: '#/components/schemas/OwnershipVerificationStatus' - description: The status of ownership verification for this account accountInfo: $ref: '#/components/schemas/ExternalAccountInfoOneOf' ExternalAccountListResponse: @@ -20550,54 +20551,203 @@ components: default: false accountInfo: $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' - OwnershipVerificationMethod: + PlatformExternalAccountCreateRequest: + type: object + required: + - currency + - accountInfo + properties: + currency: + type: string + description: The ISO 4217 currency code + example: USD + platformAccountId: + type: string + description: Your platform's identifier for the account in your system. This can be used to reference the account by your own identifier. + example: ext_acc_123456 + ownershipType: + $ref: '#/components/schemas/OwnershipType' + accountInfo: + $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' + OwnershipVerificationState: type: string enum: - - WALLET_SIGNATURE - - LIVENESS + - PENDING + - PENDING_REVIEW + - VERIFIED + - FAILED description: | - The method used to verify ownership of a self-custody crypto wallet. + Current status of this ownership verification. - | Method | Description | + | Status | Description | |--------|-------------| - | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | - | `LIVENESS` | Prove identity via a hosted biometric verification flow | - example: WALLET_SIGNATURE - OwnershipVerificationStartRequest: + | `PENDING` | Awaiting the wallet signature or the user completing the verification flow | + | `PENDING_REVIEW` | Submitted and under review | + | `VERIFIED` | Ownership was verified. Terminal | + | `FAILED` | This verification attempt failed. Terminal; start a new verification to retry | + example: PENDING + WalletSignatureOwnershipVerification: + title: Wallet Signature Ownership Verification type: object - description: Starts ownership verification for a self-custody crypto wallet external account. + description: An ownership verification completed by signing a message with the wallet's key. required: + - id + - externalAccountId - method + - status + - messageToSign + - expiresAt + - createdAt properties: + id: + type: string + description: Unique identifier for this ownership verification + example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 + externalAccountId: + type: string + description: The ID of the external account whose ownership is being verified + example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 method: - $ref: '#/components/schemas/OwnershipVerificationMethod' - description: The verification method to use. - OwnershipVerificationStart: + type: string + enum: + - WALLET_SIGNATURE + description: The verification method. Always `WALLET_SIGNATURE` for this shape. + example: WALLET_SIGNATURE + status: + $ref: '#/components/schemas/OwnershipVerificationState' + messageToSign: + type: string + description: The exact message the wallet must sign, character-for-character. + example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + expiresAt: + type: string + format: date-time + description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. + example: '2025-08-15T15:32:00Z' + createdAt: + type: string + format: date-time + description: When this verification was created + example: '2025-08-15T15:02:00Z' + updatedAt: + type: string + format: date-time + description: When this verification was last updated + example: '2025-08-15T15:02:00Z' + LivenessOwnershipVerification: + title: Liveness Ownership Verification type: object - description: 'The material needed to complete an ownership verification. Which fields are populated depends on the requested `method`: `messageToSign` for `WALLET_SIGNATURE`; `verificationLink` and `token` for `LIVENESS`.' + description: An ownership verification completed by the user through a hosted biometric verification flow. required: + - id + - externalAccountId + - method + - status + - verificationLink + - token - expiresAt + - createdAt properties: - messageToSign: + id: type: string - description: '`WALLET_SIGNATURE` only. The exact message the wallet must sign, character-for-character.' - example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + description: Unique identifier for this ownership verification + example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 + externalAccountId: + type: string + description: The ID of the external account whose ownership is being verified + example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: + type: string + enum: + - LIVENESS + description: The verification method. Always `LIVENESS` for this shape. + example: LIVENESS + status: + $ref: '#/components/schemas/OwnershipVerificationState' verificationLink: type: string format: uri - description: '`LIVENESS` only. Hosted verification URL to present to the user.' + description: Hosted verification URL to present to the user. example: https://verify.example.com/session/019542f5-b3e7-1d02 token: type: string - description: '`LIVENESS` only. Access token for embedding the verification flow in the platform''s own UI, as an alternative to `verificationLink`.' + description: Access token for embedding the verification flow in the platform's own UI, as an alternative to `verificationLink`. + example: eyJhbGciOiJIUzI1NiJ9.example expiresAt: type: string format: date-time description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. example: '2025-08-15T15:32:00Z' + createdAt: + type: string + format: date-time + description: When this verification was created + example: '2025-08-15T15:02:00Z' + updatedAt: + type: string + format: date-time + description: When this verification was last updated + example: '2025-08-15T15:02:00Z' + OwnershipVerification: + description: An ownership verification for a self-custody crypto wallet external account. The shape is determined by the verification `method`. + oneOf: + - $ref: '#/components/schemas/WalletSignatureOwnershipVerification' + - $ref: '#/components/schemas/LivenessOwnershipVerification' + discriminator: + propertyName: method + mapping: + WALLET_SIGNATURE: '#/components/schemas/WalletSignatureOwnershipVerification' + LIVENESS: '#/components/schemas/LivenessOwnershipVerification' + OwnershipVerificationListResponse: + type: object + required: + - data + - hasMore + properties: + data: + type: array + description: List of ownership verifications matching the filter criteria + items: + $ref: '#/components/schemas/OwnershipVerification' + 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 results matching the criteria + OwnershipVerificationMethod: + type: string + enum: + - WALLET_SIGNATURE + - LIVENESS + description: | + The method used to verify ownership of a self-custody crypto wallet. + + | Method | Description | + |--------|-------------| + | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | + | `LIVENESS` | Prove identity via a hosted biometric verification flow | + example: WALLET_SIGNATURE + OwnershipVerificationRequest: + type: object + description: Creates an ownership verification for a self-custody crypto wallet external account. + required: + - externalAccountId + - method + properties: + externalAccountId: + type: string + description: The ID of the external account (self-custody crypto wallet) whose ownership is being verified. + example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: + $ref: '#/components/schemas/OwnershipVerificationMethod' + description: The verification method to use. OwnershipVerificationConfirmRequest: type: object - description: Completes a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the `messageToSign` from the start step. + description: Completes a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the verification's `messageToSign`. required: - signature - signedAddress @@ -20605,6 +20755,7 @@ components: signature: type: string description: The signature produced over the exact `messageToSign` — EIP-191 hex for EVM chains, base64 for Bitcoin, base58-encoded Ed25519 for Solana. + example: '0x52d75f01c9e7b8b2ce2fbcbd21bfeeee7bcd1a2f01ce6b8ad9a67a45e83a8f5d1c' signedAddress: type: string description: The wallet address that signed the message. @@ -20616,24 +20767,6 @@ components: - electrum default: bip137 description: Bitcoin message-signing format. Defaults to `bip137`; use `electrum` for Electrum/Sparrow wallets. Ignored for non-Bitcoin chains. - PlatformExternalAccountCreateRequest: - type: object - required: - - currency - - accountInfo - properties: - currency: - type: string - description: The ISO 4217 currency code - example: USD - platformAccountId: - type: string - description: Your platform's identifier for the account in your system. This can be used to reference the account by your own identifier. - example: ext_acc_123456 - ownershipType: - $ref: '#/components/schemas/OwnershipType' - accountInfo: - $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' BeneficialOwnerListResponse: type: object required: @@ -25166,11 +25299,10 @@ components: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + - OWNERSHIP_VERIFICATION.PENDING_REVIEW + - OWNERSHIP_VERIFICATION.VERIFIED + - OWNERSHIP_VERIFICATION.FAILED + - EXTERNAL_ACCOUNT.STATUS_UPDATED - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED - INVITATION.CLAIMED @@ -25365,7 +25497,7 @@ components: enum: - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED - ExternalAccountWebhook: + ExternalAccountStatusWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' - type: object @@ -25377,11 +25509,7 @@ components: type: type: string enum: - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + - EXTERNAL_ACCOUNT.STATUS_UPDATED VerificationWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' @@ -25399,6 +25527,21 @@ components: - VERIFICATION.RESOLVE_ERRORS - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW + OwnershipVerificationWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/OwnershipVerification' + type: + type: string + enum: + - OWNERSHIP_VERIFICATION.PENDING_REVIEW + - OWNERSHIP_VERIFICATION.VERIFIED + - OWNERSHIP_VERIFICATION.FAILED CardStateChangeWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' diff --git a/openapi.yaml b/openapi.yaml index 14a893762..d8ba938a3 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -33,6 +33,8 @@ tags: description: Internal account management endpoints for creating and managing internal accounts - name: External Accounts description: External account management endpoints for creating and managing external bank accounts + - name: Ownership Verifications + description: Endpoints for verifying ownership of self-custody crypto wallet external accounts, via wallet signature or a hosted biometric verification flow. - name: Same-Currency Transfers description: Endpoints for transferring funds between internal and external accounts with the same currency - name: Cross-Currency Transfers @@ -2429,151 +2431,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /customers/external-accounts/{externalAccountId}/verify-ownership: - parameters: - - name: externalAccountId - in: path - description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string - post: - summary: Start external account ownership verification - description: | - Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet - external account. Choose a `method`: - - - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the - wallet sign it and submit the result to - `POST /customers/external-accounts/{externalAccountId}/verify-ownership/confirm` - to complete verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` (and a `token` - for embedding); the user completes a hosted biometric flow and - verification completes asynchronously. Status transitions are delivered - via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in - the account's `ownershipVerificationStatus`. - - This endpoint is only meaningful for accounts whose - `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. - operationId: verifyExternalAccountOwnership - tags: - - External Accounts - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerificationStartRequest' - responses: - '200': - description: Ownership verification started; the method-specific material is returned. - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerificationStart' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Customer or external account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: Ownership verification is not applicable to this external account. - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /customers/external-accounts/{externalAccountId}/verify-ownership/confirm: - parameters: - - name: externalAccountId - in: path - description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string - post: - summary: Confirm external account ownership verification - description: | - Complete a `WALLET_SIGNATURE` ownership verification by submitting the - signature the wallet produced for the `messageToSign` returned by - `POST /customers/external-accounts/{externalAccountId}/verify-ownership`. - The message must be signed exactly as returned, and the signature must be - submitted before the session's `expiresAt`; after expiry, start a new - verification. - - Returns the updated external account, including its new - `ownershipVerificationStatus`. - - This endpoint is only meaningful for accounts with an in-progress - `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. - operationId: confirmExternalAccountOwnershipVerification - tags: - - External Accounts - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' - responses: - '200': - description: Ownership verification submitted; the updated external account is returned. - content: - application/json: - schema: - $ref: '#/components/schemas/ExternalAccount' - '400': - description: Invalid or expired signature - content: - application/json: - schema: - $ref: '#/components/schemas/Error400' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Customer or external account not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '409': - description: Ownership verification is not applicable to this external account. - content: - application/json: - schema: - $ref: '#/components/schemas/Error409' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' /platform/external-accounts: get: summary: List platform external accounts @@ -2778,35 +2635,30 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts/{externalAccountId}/verify-ownership: - parameters: - - name: externalAccountId - in: path - description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string + /ownership-verifications: post: - summary: Start platform external account ownership verification + summary: Create an ownership verification description: | - Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet - external account owned by the platform. Choose a `method`: + Begin ownership verification for a `FIRST_PARTY` self-custody crypto + wallet external account (customer or platform owned). Choose a `method`: - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the wallet sign it and submit the result to - `POST /platform/external-accounts/{externalAccountId}/verify-ownership/confirm` - to complete verification synchronously. + `POST /ownership-verifications/{verificationId}/confirm` to complete + verification synchronously. - `LIVENESS` — the response includes a `verificationLink` (and a `token` for embedding); the user completes a hosted biometric flow and verification completes asynchronously. Status transitions are delivered - via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in - the account's `ownershipVerificationStatus`. + via `OWNERSHIP_VERIFICATION.*` webhooks or by polling + `GET /ownership-verifications/{verificationId}`. - This endpoint is only meaningful for accounts whose - `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. - operationId: verifyPlatformExternalAccountOwnership + Ownership verification applies to accounts in + `PENDING_OWNERSHIP_VERIFICATION` status; completing it moves the account + to `ACTIVE`. For accounts where ownership verification is not applicable, + this returns `409`. + operationId: createOwnershipVerification tags: - - External Accounts + - Ownership Verifications security: - BasicAuth: [] requestBody: @@ -2814,16 +2666,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerificationStartRequest' + $ref: '#/components/schemas/OwnershipVerificationRequest' responses: - '200': - description: Ownership verification started; the method-specific material is returned. + '201': + description: Ownership verification created; the method-specific material is returned. content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerificationStart' + $ref: '#/components/schemas/OwnershipVerification' '400': - description: Invalid request + description: Bad request - Invalid parameters content: application/json: schema: @@ -2852,34 +2704,136 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /platform/external-accounts/{externalAccountId}/verify-ownership/confirm: - parameters: - - name: externalAccountId - in: path - description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string + get: + summary: List ownership verifications + description: | + Retrieve a list of ownership verifications with optional filtering by external account ID and status. + operationId: listOwnershipVerifications + tags: + - Ownership Verifications + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: query + description: Filter by external account ID + required: false + schema: + type: string + - name: status + in: query + description: Filter by verification status + required: false + schema: + $ref: '#/components/schemas/OwnershipVerificationState' + - 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/OwnershipVerificationListResponse' + '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' + /ownership-verifications/{verificationId}: + get: + summary: Get an ownership verification + description: Retrieve details of a specific ownership verification by ID. + operationId: getOwnershipVerification + tags: + - Ownership Verifications + security: + - BasicAuth: [] + parameters: + - name: verificationId + in: path + description: Ownership verification ID + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerification' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error401' + '404': + description: Ownership verification not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /ownership-verifications/{verificationId}/confirm: post: - summary: Confirm platform external account ownership verification + summary: Confirm an ownership verification description: | Complete a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the `messageToSign` returned by - `POST /platform/external-accounts/{externalAccountId}/verify-ownership`. - The message must be signed exactly as returned, and the signature must be - submitted before the session's `expiresAt`; after expiry, start a new - verification. - - Returns the updated external account, including its new - `ownershipVerificationStatus`. + `POST /ownership-verifications`. The message must be signed exactly as + returned, and the signature must be submitted before the session's + `expiresAt`; after expiry, start a new verification. - This endpoint is only meaningful for accounts with an in-progress - `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. - operationId: confirmPlatformExternalAccountOwnershipVerification + This endpoint is only valid for `WALLET_SIGNATURE` verifications in + `PENDING` status. For other verifications, this returns `409`. `LIVENESS` + verifications complete asynchronously — their status is delivered via + `OWNERSHIP_VERIFICATION.*` webhooks or by polling + `GET /ownership-verifications/{verificationId}`. + operationId: confirmOwnershipVerification tags: - - External Accounts + - Ownership Verifications security: - BasicAuth: [] + parameters: + - name: verificationId + in: path + description: Ownership verification ID + required: true + schema: + type: string requestBody: required: true content: @@ -2888,11 +2842,11 @@ paths: $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' responses: '200': - description: Ownership verification submitted; the updated external account is returned. + description: Signature submitted; the updated ownership verification is returned. content: application/json: schema: - $ref: '#/components/schemas/ExternalAccount' + $ref: '#/components/schemas/OwnershipVerification' '400': description: Invalid or expired signature content: @@ -2906,13 +2860,13 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: External account not found + description: Ownership verification not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: Ownership verification is not applicable to this external account. + description: The verification is not a `WALLET_SIGNATURE` verification in `PENDING` status. content: application/json: schema: @@ -11040,11 +10994,11 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' - external-account: + external-account-status: post: - summary: External account ownership verification status change + summary: External account status webhook description: | - Webhook that is called when the ownership verification status of an external account changes. + Webhook that is called when the status of an external account changes (e.g., `PENDING_OWNERSHIP_VERIFICATION` → `ACTIVE` after ownership verification completes). This endpoint should be implemented by clients of the Grid API. ### Authentication @@ -11058,12 +11012,8 @@ webhooks: If the signature verification succeeds, the webhook is authentic. If not, it should be rejected. ### Event types - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED` — Fired when an external account is created and ownership verification does not apply to it. The `data` payload contains the full external account object. - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED` — Fired when an external account requires ownership verification before transfers above regulatory thresholds can be sent to it. The `data` payload contains the full external account object. - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full external account object. - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full external account object. - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED` — Fired when an ownership verification attempt fails; a new verification can be started. The `data` payload contains the full external account object. - operationId: externalAccountWebhook + - `EXTERNAL_ACCOUNT.STATUS_UPDATED` — Fired when the status of an external account changes. The `data` payload contains the full external account object. + operationId: externalAccountStatusWebhook tags: - Webhooks security: @@ -11073,29 +11023,13 @@ webhooks: content: application/json: schema: - $ref: '#/components/schemas/ExternalAccountWebhook' + $ref: '#/components/schemas/ExternalAccountStatusWebhook' examples: - ownershipVerified: - summary: Ownership of a self-custody wallet has been verified - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000040 - type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - timestamp: '2025-08-15T14:32:00Z' - data: - id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 - status: ACTIVE - currency: USDC - ownershipType: FIRST_PARTY - ownershipVerificationStatus: VERIFIED - accountInfo: - accountType: ETHEREUM_WALLET - address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' - ownershipVerificationFailed: - summary: An ownership verification attempt failed + statusUpdated: + summary: A wallet account became active after ownership verification value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000041 - type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + id: Webhook:019542f5-b3e7-1d02-0000-000000000042 + type: EXTERNAL_ACCOUNT.STATUS_UPDATED timestamp: '2025-08-15T14:32:00Z' data: id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 @@ -11103,7 +11037,6 @@ webhooks: status: ACTIVE currency: USDC ownershipType: FIRST_PARTY - ownershipVerificationStatus: FAILED accountInfo: accountType: ETHEREUM_WALLET address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' @@ -11208,6 +11141,92 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + ownership-verification: + post: + summary: Ownership verification status change + description: | + Webhook that is called when the status of an ownership verification 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. + + ### Event types + - `OWNERSHIP_VERIFICATION.PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full ownership verification object. + - `OWNERSHIP_VERIFICATION.VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full ownership verification object. + - `OWNERSHIP_VERIFICATION.FAILED` — Fired when an ownership verification attempt fails; start a new verification to retry. The `data` payload contains the full ownership verification object. + operationId: ownershipVerificationWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerificationWebhook' + examples: + verified: + summary: Ownership of a self-custody wallet has been verified + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000040 + type: OWNERSHIP_VERIFICATION.VERIFIED + timestamp: '2025-08-15T14:32:00Z' + data: + id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 + externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: WALLET_SIGNATURE + status: VERIFIED + messageToSign: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + expiresAt: '2025-08-15T15:32:00Z' + createdAt: '2025-08-15T15:02:00Z' + updatedAt: '2025-08-15T14:32:00Z' + failed: + summary: An ownership verification attempt failed + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000041 + type: OWNERSHIP_VERIFICATION.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 + externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: LIVENESS + status: FAILED + verificationLink: https://verify.example.com/session/019542f5-b3e7-1d02 + token: eyJhbGciOiJIUzI1NiJ9.example + expiresAt: '2025-08-15T15:32:00Z' + createdAt: '2025-08-15T15:02:00Z' + updatedAt: '2025-08-15T14:32:00Z' + 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' card-state-change: post: summary: Card state change @@ -12193,7 +12212,7 @@ components: | INVALID_PUBKEY_FORMAT | Counterparty Public key format is invalid | | MISSING_REQUIRED_UMA_PARAMETERS | Counterparty required UMA parameters are missing | | SENDER_NOT_ACCEPTED | Sender is not accepted | - | AMOUNT_OUT_OF_RANGE | Amount is out of range | + | AMOUNT_OUT_OF_RANGE | Amount is out of range for the quote or the destination account | | INVALID_CURRENCY | Currency is invalid | | INVALID_TIMESTAMP | Timestamp is invalid | | INVALID_NONCE | Nonce is invalid | @@ -12217,10 +12236,7 @@ components: | STABLECOIN_PROVIDER_ACCOUNT_INVALID | The stablecoin provider account link is not usable | | STABLECOIN_PROVIDER_ACCOUNT_REVOKED | The stablecoin provider account link has been revoked | | STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED | Multiple active provider account links exist; pass `stablecoinProviderAccountId` to select one | - | OWNERSHIP_TYPE_REQUIRED | `ownershipType` must be provided for this external account | | WALLET_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | - | THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for third-party accounts | - | LIGHTNING_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for this destination type | enum: - INVALID_INPUT - END_USER_TERMS_VERSION_NOT_FOUND @@ -12262,10 +12278,7 @@ components: - STABLECOIN_PROVIDER_ACCOUNT_INVALID - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED - - OWNERSHIP_TYPE_REQUIRED - WALLET_VERIFICATION_REQUIRED - - THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED - - LIGHTNING_TRANSFER_LIMIT_EXCEEDED message: type: string description: Error message @@ -17227,9 +17240,18 @@ components: enum: - PENDING - ACTIVE + - PENDING_OWNERSHIP_VERIFICATION - UNDER_REVIEW - INACTIVE - description: Status of an external account + description: | + Status of an external account. + + `PENDING_OWNERSHIP_VERIFICATION` applies to `FIRST_PARTY` self-custody + crypto wallet accounts on platforms subject to EU Travel Rule requirements. + While in this status, the account can be used for transfers below regulatory + thresholds; completing ownership verification (see the Ownership + Verifications API) moves the account to `ACTIVE` and removes the + restriction. OwnershipType: type: string enum: @@ -17264,24 +17286,6 @@ components: type: string description: The verified full name of the account holder as returned by the payment rail example: John Doe - OwnershipVerificationStatus: - type: string - enum: - - NOT_REQUIRED - - REQUIRED - - PENDING_REVIEW - - FAILED - - VERIFIED - description: | - The status of ownership verification for this external account. - - | Status | Description | - |--------|-------------| - | `NOT_REQUIRED` | Ownership verification does not apply to this account | - | `REQUIRED` | Ownership must be verified before transfers above regulatory thresholds can be sent to this account | - | `PENDING_REVIEW` | A verification was submitted and is under review | - | `FAILED` | The most recent verification attempt failed; a new verification can be started | - | `VERIFIED` | Ownership has been verified; no further action is needed | ExternalAccountType: type: string enum: @@ -19641,9 +19645,6 @@ components: beneficiaryVerifiedData: $ref: '#/components/schemas/BeneficiaryVerifiedData' description: Verified beneficiary data returned by the payment rail, if available - ownershipVerificationStatus: - $ref: '#/components/schemas/OwnershipVerificationStatus' - description: The status of ownership verification for this account accountInfo: $ref: '#/components/schemas/ExternalAccountInfoOneOf' ExternalAccountListResponse: @@ -20550,54 +20551,203 @@ components: default: false accountInfo: $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' - OwnershipVerificationMethod: + PlatformExternalAccountCreateRequest: + type: object + required: + - currency + - accountInfo + properties: + currency: + type: string + description: The ISO 4217 currency code + example: USD + platformAccountId: + type: string + description: Your platform's identifier for the account in your system. This can be used to reference the account by your own identifier. + example: ext_acc_123456 + ownershipType: + $ref: '#/components/schemas/OwnershipType' + accountInfo: + $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' + OwnershipVerificationState: type: string enum: - - WALLET_SIGNATURE - - LIVENESS + - PENDING + - PENDING_REVIEW + - VERIFIED + - FAILED description: | - The method used to verify ownership of a self-custody crypto wallet. + Current status of this ownership verification. - | Method | Description | + | Status | Description | |--------|-------------| - | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | - | `LIVENESS` | Prove identity via a hosted biometric verification flow | - example: WALLET_SIGNATURE - OwnershipVerificationStartRequest: + | `PENDING` | Awaiting the wallet signature or the user completing the verification flow | + | `PENDING_REVIEW` | Submitted and under review | + | `VERIFIED` | Ownership was verified. Terminal | + | `FAILED` | This verification attempt failed. Terminal; start a new verification to retry | + example: PENDING + WalletSignatureOwnershipVerification: + title: Wallet Signature Ownership Verification type: object - description: Starts ownership verification for a self-custody crypto wallet external account. + description: An ownership verification completed by signing a message with the wallet's key. required: + - id + - externalAccountId - method + - status + - messageToSign + - expiresAt + - createdAt properties: + id: + type: string + description: Unique identifier for this ownership verification + example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 + externalAccountId: + type: string + description: The ID of the external account whose ownership is being verified + example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 method: - $ref: '#/components/schemas/OwnershipVerificationMethod' - description: The verification method to use. - OwnershipVerificationStart: + type: string + enum: + - WALLET_SIGNATURE + description: The verification method. Always `WALLET_SIGNATURE` for this shape. + example: WALLET_SIGNATURE + status: + $ref: '#/components/schemas/OwnershipVerificationState' + messageToSign: + type: string + description: The exact message the wallet must sign, character-for-character. + example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + expiresAt: + type: string + format: date-time + description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. + example: '2025-08-15T15:32:00Z' + createdAt: + type: string + format: date-time + description: When this verification was created + example: '2025-08-15T15:02:00Z' + updatedAt: + type: string + format: date-time + description: When this verification was last updated + example: '2025-08-15T15:02:00Z' + LivenessOwnershipVerification: + title: Liveness Ownership Verification type: object - description: 'The material needed to complete an ownership verification. Which fields are populated depends on the requested `method`: `messageToSign` for `WALLET_SIGNATURE`; `verificationLink` and `token` for `LIVENESS`.' + description: An ownership verification completed by the user through a hosted biometric verification flow. required: + - id + - externalAccountId + - method + - status + - verificationLink + - token - expiresAt + - createdAt properties: - messageToSign: + id: type: string - description: '`WALLET_SIGNATURE` only. The exact message the wallet must sign, character-for-character.' - example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + description: Unique identifier for this ownership verification + example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 + externalAccountId: + type: string + description: The ID of the external account whose ownership is being verified + example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: + type: string + enum: + - LIVENESS + description: The verification method. Always `LIVENESS` for this shape. + example: LIVENESS + status: + $ref: '#/components/schemas/OwnershipVerificationState' verificationLink: type: string format: uri - description: '`LIVENESS` only. Hosted verification URL to present to the user.' + description: Hosted verification URL to present to the user. example: https://verify.example.com/session/019542f5-b3e7-1d02 token: type: string - description: '`LIVENESS` only. Access token for embedding the verification flow in the platform''s own UI, as an alternative to `verificationLink`.' + description: Access token for embedding the verification flow in the platform's own UI, as an alternative to `verificationLink`. + example: eyJhbGciOiJIUzI1NiJ9.example expiresAt: type: string format: date-time description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. example: '2025-08-15T15:32:00Z' + createdAt: + type: string + format: date-time + description: When this verification was created + example: '2025-08-15T15:02:00Z' + updatedAt: + type: string + format: date-time + description: When this verification was last updated + example: '2025-08-15T15:02:00Z' + OwnershipVerification: + description: An ownership verification for a self-custody crypto wallet external account. The shape is determined by the verification `method`. + oneOf: + - $ref: '#/components/schemas/WalletSignatureOwnershipVerification' + - $ref: '#/components/schemas/LivenessOwnershipVerification' + discriminator: + propertyName: method + mapping: + WALLET_SIGNATURE: '#/components/schemas/WalletSignatureOwnershipVerification' + LIVENESS: '#/components/schemas/LivenessOwnershipVerification' + OwnershipVerificationListResponse: + type: object + required: + - data + - hasMore + properties: + data: + type: array + description: List of ownership verifications matching the filter criteria + items: + $ref: '#/components/schemas/OwnershipVerification' + 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 results matching the criteria + OwnershipVerificationMethod: + type: string + enum: + - WALLET_SIGNATURE + - LIVENESS + description: | + The method used to verify ownership of a self-custody crypto wallet. + + | Method | Description | + |--------|-------------| + | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | + | `LIVENESS` | Prove identity via a hosted biometric verification flow | + example: WALLET_SIGNATURE + OwnershipVerificationRequest: + type: object + description: Creates an ownership verification for a self-custody crypto wallet external account. + required: + - externalAccountId + - method + properties: + externalAccountId: + type: string + description: The ID of the external account (self-custody crypto wallet) whose ownership is being verified. + example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: + $ref: '#/components/schemas/OwnershipVerificationMethod' + description: The verification method to use. OwnershipVerificationConfirmRequest: type: object - description: Completes a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the `messageToSign` from the start step. + description: Completes a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the verification's `messageToSign`. required: - signature - signedAddress @@ -20605,6 +20755,7 @@ components: signature: type: string description: The signature produced over the exact `messageToSign` — EIP-191 hex for EVM chains, base64 for Bitcoin, base58-encoded Ed25519 for Solana. + example: '0x52d75f01c9e7b8b2ce2fbcbd21bfeeee7bcd1a2f01ce6b8ad9a67a45e83a8f5d1c' signedAddress: type: string description: The wallet address that signed the message. @@ -20616,24 +20767,6 @@ components: - electrum default: bip137 description: Bitcoin message-signing format. Defaults to `bip137`; use `electrum` for Electrum/Sparrow wallets. Ignored for non-Bitcoin chains. - PlatformExternalAccountCreateRequest: - type: object - required: - - currency - - accountInfo - properties: - currency: - type: string - description: The ISO 4217 currency code - example: USD - platformAccountId: - type: string - description: Your platform's identifier for the account in your system. This can be used to reference the account by your own identifier. - example: ext_acc_123456 - ownershipType: - $ref: '#/components/schemas/OwnershipType' - accountInfo: - $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' BeneficialOwnerListResponse: type: object required: @@ -25166,11 +25299,10 @@ components: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + - OWNERSHIP_VERIFICATION.PENDING_REVIEW + - OWNERSHIP_VERIFICATION.VERIFIED + - OWNERSHIP_VERIFICATION.FAILED + - EXTERNAL_ACCOUNT.STATUS_UPDATED - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED - INVITATION.CLAIMED @@ -25365,7 +25497,7 @@ components: enum: - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED - ExternalAccountWebhook: + ExternalAccountStatusWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' - type: object @@ -25377,11 +25509,7 @@ components: type: type: string enum: - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + - EXTERNAL_ACCOUNT.STATUS_UPDATED VerificationWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' @@ -25399,6 +25527,21 @@ components: - VERIFICATION.RESOLVE_ERRORS - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW + OwnershipVerificationWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/OwnershipVerification' + type: + type: string + enum: + - OWNERSHIP_VERIFICATION.PENDING_REVIEW + - OWNERSHIP_VERIFICATION.VERIFIED + - OWNERSHIP_VERIFICATION.FAILED CardStateChangeWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' diff --git a/openapi/components/schemas/errors/Error400.yaml b/openapi/components/schemas/errors/Error400.yaml index dc073dccd..6185df648 100644 --- a/openapi/components/schemas/errors/Error400.yaml +++ b/openapi/components/schemas/errors/Error400.yaml @@ -30,7 +30,7 @@ properties: | INVALID_PUBKEY_FORMAT | Counterparty Public key format is invalid | | MISSING_REQUIRED_UMA_PARAMETERS | Counterparty required UMA parameters are missing | | SENDER_NOT_ACCEPTED | Sender is not accepted | - | AMOUNT_OUT_OF_RANGE | Amount is out of range | + | AMOUNT_OUT_OF_RANGE | Amount is out of range for the quote or the destination account | | INVALID_CURRENCY | Currency is invalid | | INVALID_TIMESTAMP | Timestamp is invalid | | INVALID_NONCE | Nonce is invalid | @@ -54,10 +54,7 @@ properties: | STABLECOIN_PROVIDER_ACCOUNT_INVALID | The stablecoin provider account link is not usable | | STABLECOIN_PROVIDER_ACCOUNT_REVOKED | The stablecoin provider account link has been revoked | | STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED | Multiple active provider account links exist; pass `stablecoinProviderAccountId` to select one | - | OWNERSHIP_TYPE_REQUIRED | `ownershipType` must be provided for this external account | | WALLET_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | - | THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for third-party accounts | - | LIGHTNING_TRANSFER_LIMIT_EXCEEDED | The transfer exceeds the permitted amount for this destination type | enum: - INVALID_INPUT - END_USER_TERMS_VERSION_NOT_FOUND @@ -99,10 +96,7 @@ properties: - STABLECOIN_PROVIDER_ACCOUNT_INVALID - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED - - OWNERSHIP_TYPE_REQUIRED - WALLET_VERIFICATION_REQUIRED - - THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED - - LIGHTNING_TRANSFER_LIMIT_EXCEEDED message: type: string description: Error message diff --git a/openapi/components/schemas/external_accounts/ExternalAccount.yaml b/openapi/components/schemas/external_accounts/ExternalAccount.yaml index aa2099cdb..08449b833 100644 --- a/openapi/components/schemas/external_accounts/ExternalAccount.yaml +++ b/openapi/components/schemas/external_accounts/ExternalAccount.yaml @@ -42,8 +42,5 @@ allOf: beneficiaryVerifiedData: $ref: ./BeneficiaryVerifiedData.yaml description: Verified beneficiary data returned by the payment rail, if available - ownershipVerificationStatus: - $ref: ./OwnershipVerificationStatus.yaml - description: The status of ownership verification for this account accountInfo: $ref: ./ExternalAccountInfoOneOf.yaml diff --git a/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml b/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml index bc3495857..5b62fd215 100644 --- a/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml +++ b/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml @@ -2,6 +2,15 @@ type: string enum: - PENDING - ACTIVE + - PENDING_OWNERSHIP_VERIFICATION - UNDER_REVIEW - INACTIVE -description: Status of an external account +description: | + Status of an external account. + + `PENDING_OWNERSHIP_VERIFICATION` applies to `FIRST_PARTY` self-custody + crypto wallet accounts on platforms subject to EU Travel Rule requirements. + While in this status, the account can be used for transfers below regulatory + thresholds; completing ownership verification (see the Ownership + Verifications API) moves the account to `ACTIVE` and removes the + restriction. diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationStart.yaml b/openapi/components/schemas/external_accounts/OwnershipVerificationStart.yaml deleted file mode 100644 index 2987e0b97..000000000 --- a/openapi/components/schemas/external_accounts/OwnershipVerificationStart.yaml +++ /dev/null @@ -1,32 +0,0 @@ -type: object -description: >- - The material needed to complete an ownership verification. Which fields are - populated depends on the requested `method`: `messageToSign` for - `WALLET_SIGNATURE`; `verificationLink` and `token` for `LIVENESS`. -required: - - expiresAt -properties: - messageToSign: - type: string - description: >- - `WALLET_SIGNATURE` only. The exact message the wallet must sign, - character-for-character. - example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' - verificationLink: - type: string - format: uri - description: >- - `LIVENESS` only. Hosted verification URL to present to the user. - example: https://verify.example.com/session/019542f5-b3e7-1d02 - token: - type: string - description: >- - `LIVENESS` only. Access token for embedding the verification flow in the - platform's own UI, as an alternative to `verificationLink`. - expiresAt: - type: string - format: date-time - description: >- - When this verification session expires. Prompt the user promptly; after - expiry, a new verification must be started. - example: '2025-08-15T15:32:00Z' diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml b/openapi/components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml deleted file mode 100644 index 447fc9da4..000000000 --- a/openapi/components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml +++ /dev/null @@ -1,10 +0,0 @@ -type: object -description: >- - Starts ownership verification for a self-custody crypto wallet external - account. -required: - - method -properties: - method: - $ref: ./OwnershipVerificationMethod.yaml - description: The verification method to use. diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationStatus.yaml b/openapi/components/schemas/external_accounts/OwnershipVerificationStatus.yaml deleted file mode 100644 index 199c3ffa0..000000000 --- a/openapi/components/schemas/external_accounts/OwnershipVerificationStatus.yaml +++ /dev/null @@ -1,17 +0,0 @@ -type: string -enum: - - NOT_REQUIRED - - REQUIRED - - PENDING_REVIEW - - FAILED - - VERIFIED -description: | - The status of ownership verification for this external account. - - | Status | Description | - |--------|-------------| - | `NOT_REQUIRED` | Ownership verification does not apply to this account | - | `REQUIRED` | Ownership must be verified before transfers above regulatory thresholds can be sent to this account | - | `PENDING_REVIEW` | A verification was submitted and is under review | - | `FAILED` | The most recent verification attempt failed; a new verification can be started | - | `VERIFIED` | Ownership has been verified; no further action is needed | diff --git a/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml b/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml new file mode 100644 index 000000000..cbb4db8a5 --- /dev/null +++ b/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml @@ -0,0 +1,59 @@ +title: Liveness Ownership Verification +type: object +description: >- + An ownership verification completed by the user through a hosted biometric + verification flow. +required: + - id + - externalAccountId + - method + - status + - verificationLink + - token + - expiresAt + - createdAt +properties: + id: + type: string + description: Unique identifier for this ownership verification + example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 + externalAccountId: + type: string + description: The ID of the external account whose ownership is being verified + example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: + type: string + enum: + - LIVENESS + description: The verification method. Always `LIVENESS` for this shape. + example: LIVENESS + status: + $ref: ./OwnershipVerificationState.yaml + verificationLink: + type: string + format: uri + description: Hosted verification URL to present to the user. + example: https://verify.example.com/session/019542f5-b3e7-1d02 + token: + type: string + description: >- + Access token for embedding the verification flow in the platform's own + UI, as an alternative to `verificationLink`. + example: eyJhbGciOiJIUzI1NiJ9.example + expiresAt: + type: string + format: date-time + description: >- + When this verification session expires. Prompt the user promptly; after + expiry, a new verification must be started. + example: '2025-08-15T15:32:00Z' + createdAt: + type: string + format: date-time + description: When this verification was created + example: '2025-08-15T15:02:00Z' + updatedAt: + type: string + format: date-time + description: When this verification was last updated + example: '2025-08-15T15:02:00Z' diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerification.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerification.yaml new file mode 100644 index 000000000..827239d18 --- /dev/null +++ b/openapi/components/schemas/ownership_verifications/OwnershipVerification.yaml @@ -0,0 +1,11 @@ +description: >- + An ownership verification for a self-custody crypto wallet external account. + The shape is determined by the verification `method`. +oneOf: + - $ref: ./WalletSignatureOwnershipVerification.yaml + - $ref: ./LivenessOwnershipVerification.yaml +discriminator: + propertyName: method + mapping: + WALLET_SIGNATURE: ./WalletSignatureOwnershipVerification.yaml + LIVENESS: ./LivenessOwnershipVerification.yaml diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml similarity index 83% rename from openapi/components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml rename to openapi/components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml index 25c675387..3f275e2de 100644 --- a/openapi/components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml +++ b/openapi/components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml @@ -1,7 +1,7 @@ type: object description: >- Completes a `WALLET_SIGNATURE` ownership verification by submitting the - signature the wallet produced for the `messageToSign` from the start step. + signature the wallet produced for the verification's `messageToSign`. required: - signature - signedAddress @@ -11,6 +11,7 @@ properties: description: >- The signature produced over the exact `messageToSign` — EIP-191 hex for EVM chains, base64 for Bitcoin, base58-encoded Ed25519 for Solana. + example: '0x52d75f01c9e7b8b2ce2fbcbd21bfeeee7bcd1a2f01ce6b8ad9a67a45e83a8f5d1c' signedAddress: type: string description: The wallet address that signed the message. diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerificationListResponse.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationListResponse.yaml new file mode 100644 index 000000000..87bfc4b93 --- /dev/null +++ b/openapi/components/schemas/ownership_verifications/OwnershipVerificationListResponse.yaml @@ -0,0 +1,21 @@ +type: object +required: + - data + - hasMore +properties: + data: + type: array + description: List of ownership verifications matching the filter criteria + items: + $ref: ./OwnershipVerification.yaml + 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 results matching the criteria diff --git a/openapi/components/schemas/external_accounts/OwnershipVerificationMethod.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationMethod.yaml similarity index 100% rename from openapi/components/schemas/external_accounts/OwnershipVerificationMethod.yaml rename to openapi/components/schemas/ownership_verifications/OwnershipVerificationMethod.yaml diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml new file mode 100644 index 000000000..d1feafe02 --- /dev/null +++ b/openapi/components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml @@ -0,0 +1,17 @@ +type: object +description: >- + Creates an ownership verification for a self-custody crypto wallet external + account. +required: + - externalAccountId + - method +properties: + externalAccountId: + type: string + description: >- + The ID of the external account (self-custody crypto wallet) whose + ownership is being verified. + example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: + $ref: ./OwnershipVerificationMethod.yaml + description: The verification method to use. diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerificationState.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationState.yaml new file mode 100644 index 000000000..3bbfb25fa --- /dev/null +++ b/openapi/components/schemas/ownership_verifications/OwnershipVerificationState.yaml @@ -0,0 +1,16 @@ +type: string +enum: + - PENDING + - PENDING_REVIEW + - VERIFIED + - FAILED +description: | + Current status of this ownership verification. + + | Status | Description | + |--------|-------------| + | `PENDING` | Awaiting the wallet signature or the user completing the verification flow | + | `PENDING_REVIEW` | Submitted and under review | + | `VERIFIED` | Ownership was verified. Terminal | + | `FAILED` | This verification attempt failed. Terminal; start a new verification to retry | +example: PENDING diff --git a/openapi/components/schemas/ownership_verifications/WalletSignatureOwnershipVerification.yaml b/openapi/components/schemas/ownership_verifications/WalletSignatureOwnershipVerification.yaml new file mode 100644 index 000000000..f5c9219a3 --- /dev/null +++ b/openapi/components/schemas/ownership_verifications/WalletSignatureOwnershipVerification.yaml @@ -0,0 +1,51 @@ +title: Wallet Signature Ownership Verification +type: object +description: >- + An ownership verification completed by signing a message with the wallet's + key. +required: + - id + - externalAccountId + - method + - status + - messageToSign + - expiresAt + - createdAt +properties: + id: + type: string + description: Unique identifier for this ownership verification + example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 + externalAccountId: + type: string + description: The ID of the external account whose ownership is being verified + example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: + type: string + enum: + - WALLET_SIGNATURE + description: The verification method. Always `WALLET_SIGNATURE` for this shape. + example: WALLET_SIGNATURE + status: + $ref: ./OwnershipVerificationState.yaml + messageToSign: + type: string + description: The exact message the wallet must sign, character-for-character. + example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + expiresAt: + type: string + format: date-time + description: >- + When this verification session expires. Prompt the user promptly; after + expiry, a new verification must be started. + example: '2025-08-15T15:32:00Z' + createdAt: + type: string + format: date-time + description: When this verification was created + example: '2025-08-15T15:02:00Z' + updatedAt: + type: string + format: date-time + description: When this verification was last updated + example: '2025-08-15T15:02:00Z' diff --git a/openapi/components/schemas/webhooks/ExternalAccountStatusWebhook.yaml b/openapi/components/schemas/webhooks/ExternalAccountStatusWebhook.yaml new file mode 100644 index 000000000..6b90db8eb --- /dev/null +++ b/openapi/components/schemas/webhooks/ExternalAccountStatusWebhook.yaml @@ -0,0 +1,12 @@ +allOf: + - $ref: ./BaseWebhook.yaml + - type: object + required: + - data + properties: + data: + $ref: ../external_accounts/ExternalAccount.yaml + type: + type: string + enum: + - EXTERNAL_ACCOUNT.STATUS_UPDATED diff --git a/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml b/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml deleted file mode 100644 index 136428934..000000000 --- a/openapi/components/schemas/webhooks/ExternalAccountWebhook.yaml +++ /dev/null @@ -1,16 +0,0 @@ -allOf: - - $ref: ./BaseWebhook.yaml - - type: object - required: - - data - properties: - data: - $ref: ../external_accounts/ExternalAccount.yaml - type: - type: string - enum: - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED diff --git a/openapi/components/schemas/webhooks/OwnershipVerificationWebhook.yaml b/openapi/components/schemas/webhooks/OwnershipVerificationWebhook.yaml new file mode 100644 index 000000000..3b714288a --- /dev/null +++ b/openapi/components/schemas/webhooks/OwnershipVerificationWebhook.yaml @@ -0,0 +1,14 @@ +allOf: + - $ref: ./BaseWebhook.yaml + - type: object + required: + - data + properties: + data: + $ref: ../ownership_verifications/OwnershipVerification.yaml + type: + type: string + enum: + - OWNERSHIP_VERIFICATION.PENDING_REVIEW + - OWNERSHIP_VERIFICATION.VERIFIED + - OWNERSHIP_VERIFICATION.FAILED diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml index 80ad2f95d..3844b5725 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -26,11 +26,10 @@ enum: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - - EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED + - OWNERSHIP_VERIFICATION.PENDING_REVIEW + - OWNERSHIP_VERIFICATION.VERIFIED + - OWNERSHIP_VERIFICATION.FAILED + - EXTERNAL_ACCOUNT.STATUS_UPDATED - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED - INVITATION.CLAIMED diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index db1c9dd0d..8156b8482 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -40,6 +40,10 @@ tags: description: Internal account management endpoints for creating and managing internal accounts - name: External Accounts description: External account management endpoints for creating and managing external bank accounts + - name: Ownership Verifications + description: >- + Endpoints for verifying ownership of self-custody crypto wallet external + accounts, via wallet signature or a hosted biometric verification flow. - name: Same-Currency Transfers description: Endpoints for transferring funds between internal and external accounts with the same currency - name: Cross-Currency Transfers @@ -189,18 +193,16 @@ paths: $ref: paths/customers/customers_external_accounts.yaml /customers/external-accounts/{externalAccountId}: $ref: paths/customers/customers_external_accounts_{externalAccountId}.yaml - /customers/external-accounts/{externalAccountId}/verify-ownership: - $ref: paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml - /customers/external-accounts/{externalAccountId}/verify-ownership/confirm: - $ref: paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml /platform/external-accounts: $ref: paths/platform/platform_external_accounts.yaml /platform/external-accounts/{externalAccountId}: $ref: paths/platform/platform_external_accounts_{externalAccountId}.yaml - /platform/external-accounts/{externalAccountId}/verify-ownership: - $ref: paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml - /platform/external-accounts/{externalAccountId}/verify-ownership/confirm: - $ref: paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml + /ownership-verifications: + $ref: paths/ownership_verifications/ownership-verifications.yaml + /ownership-verifications/{verificationId}: + $ref: paths/ownership_verifications/ownership-verifications_{verificationId}.yaml + /ownership-verifications/{verificationId}/confirm: + $ref: paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml /beneficial-owners: $ref: paths/beneficial-owners/beneficial_owners.yaml /beneficial-owners/{beneficialOwnerId}: @@ -397,10 +399,12 @@ webhooks: $ref: webhooks/customer-update.yaml internal-account-status: $ref: webhooks/internal-account-status.yaml - external-account: - $ref: webhooks/external-account.yaml + external-account-status: + $ref: webhooks/external-account-status.yaml verification-update: $ref: webhooks/verification-update.yaml + ownership-verification: + $ref: webhooks/ownership-verification.yaml card-state-change: $ref: webhooks/card-state-change.yaml card-funding-source-change: diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml deleted file mode 100644 index 2a9dcceec..000000000 --- a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership.yaml +++ /dev/null @@ -1,73 +0,0 @@ -parameters: - - name: externalAccountId - in: path - description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string -post: - summary: Start external account ownership verification - description: | - Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet - external account. Choose a `method`: - - - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the - wallet sign it and submit the result to - `POST /customers/external-accounts/{externalAccountId}/verify-ownership/confirm` - to complete verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` (and a `token` - for embedding); the user completes a hosted biometric flow and - verification completes asynchronously. Status transitions are delivered - via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in - the account's `ownershipVerificationStatus`. - - This endpoint is only meaningful for accounts whose - `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. - operationId: verifyExternalAccountOwnership - tags: - - External Accounts - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml - responses: - '200': - description: Ownership verification started; the method-specific material is returned. - content: - application/json: - schema: - $ref: ../../components/schemas/external_accounts/OwnershipVerificationStart.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: Ownership verification is not applicable to this external account. - 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}_verify-ownership_confirm.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml deleted file mode 100644 index e5895f3f3..000000000 --- a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml +++ /dev/null @@ -1,70 +0,0 @@ -parameters: - - name: externalAccountId - in: path - description: The unique identifier of the external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string -post: - summary: Confirm external account ownership verification - description: | - Complete a `WALLET_SIGNATURE` ownership verification by submitting the - signature the wallet produced for the `messageToSign` returned by - `POST /customers/external-accounts/{externalAccountId}/verify-ownership`. - The message must be signed exactly as returned, and the signature must be - submitted before the session's `expiresAt`; after expiry, start a new - verification. - - Returns the updated external account, including its new - `ownershipVerificationStatus`. - - This endpoint is only meaningful for accounts with an in-progress - `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. - operationId: confirmExternalAccountOwnershipVerification - tags: - - External Accounts - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml - responses: - '200': - description: Ownership verification submitted; the updated external account is returned. - content: - application/json: - schema: - $ref: ../../components/schemas/external_accounts/ExternalAccount.yaml - '400': - description: Invalid or expired signature - 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: Ownership verification is not applicable to this external account. - 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/ownership_verifications/ownership-verifications.yaml b/openapi/paths/ownership_verifications/ownership-verifications.yaml new file mode 100644 index 000000000..fe0b2f100 --- /dev/null +++ b/openapi/paths/ownership_verifications/ownership-verifications.yaml @@ -0,0 +1,133 @@ +post: + summary: Create an ownership verification + description: | + Begin ownership verification for a `FIRST_PARTY` self-custody crypto + wallet external account (customer or platform owned). Choose a `method`: + + - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the + wallet sign it and submit the result to + `POST /ownership-verifications/{verificationId}/confirm` to complete + verification synchronously. + - `LIVENESS` — the response includes a `verificationLink` (and a `token` + for embedding); the user completes a hosted biometric flow and + verification completes asynchronously. Status transitions are delivered + via `OWNERSHIP_VERIFICATION.*` webhooks or by polling + `GET /ownership-verifications/{verificationId}`. + + Ownership verification applies to accounts in + `PENDING_OWNERSHIP_VERIFICATION` status; completing it moves the account + to `ACTIVE`. For accounts where ownership verification is not applicable, + this returns `409`. + operationId: createOwnershipVerification + tags: + - Ownership Verifications + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml + responses: + '201': + description: >- + Ownership verification created; the method-specific material is + returned. + content: + application/json: + schema: + $ref: ../../components/schemas/ownership_verifications/OwnershipVerification.yaml + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: External account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: Ownership verification is not applicable to this external account. + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error409.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml +get: + summary: List ownership verifications + description: > + Retrieve a list of ownership verifications with optional filtering by + external account ID and status. + operationId: listOwnershipVerifications + tags: + - Ownership Verifications + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: query + description: Filter by external account ID + required: false + schema: + type: string + - name: status + in: query + description: Filter by verification status + required: false + schema: + $ref: ../../components/schemas/ownership_verifications/OwnershipVerificationState.yaml + - 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/ownership_verifications/OwnershipVerificationListResponse.yaml + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}.yaml b/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}.yaml new file mode 100644 index 000000000..3e608aea9 --- /dev/null +++ b/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}.yaml @@ -0,0 +1,40 @@ +get: + summary: Get an ownership verification + description: Retrieve details of a specific ownership verification by ID. + operationId: getOwnershipVerification + tags: + - Ownership Verifications + security: + - BasicAuth: [] + parameters: + - name: verificationId + in: path + description: Ownership verification ID + required: true + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: ../../components/schemas/ownership_verifications/OwnershipVerification.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: Ownership verification not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '500': + description: Internal service error + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml b/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml new file mode 100644 index 000000000..bb686e60b --- /dev/null +++ b/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml @@ -0,0 +1,72 @@ +post: + summary: Confirm an ownership verification + description: | + Complete a `WALLET_SIGNATURE` ownership verification by submitting the + signature the wallet produced for the `messageToSign` returned by + `POST /ownership-verifications`. The message must be signed exactly as + returned, and the signature must be submitted before the session's + `expiresAt`; after expiry, start a new verification. + + This endpoint is only valid for `WALLET_SIGNATURE` verifications in + `PENDING` status. For other verifications, this returns `409`. `LIVENESS` + verifications complete asynchronously — their status is delivered via + `OWNERSHIP_VERIFICATION.*` webhooks or by polling + `GET /ownership-verifications/{verificationId}`. + operationId: confirmOwnershipVerification + tags: + - Ownership Verifications + security: + - BasicAuth: [] + parameters: + - name: verificationId + in: path + description: Ownership verification ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml + responses: + '200': + description: >- + Signature submitted; the updated ownership verification is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/ownership_verifications/OwnershipVerification.yaml + '400': + description: Invalid or expired signature + 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: Ownership verification not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + The verification is not a `WALLET_SIGNATURE` verification in `PENDING` + status. + 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/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml deleted file mode 100644 index 993e275dc..000000000 --- a/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership.yaml +++ /dev/null @@ -1,73 +0,0 @@ -parameters: - - name: externalAccountId - in: path - description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string -post: - summary: Start platform external account ownership verification - description: | - Begin ownership verification for a `FIRST_PARTY` self-custody crypto wallet - external account owned by the platform. Choose a `method`: - - - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the - wallet sign it and submit the result to - `POST /platform/external-accounts/{externalAccountId}/verify-ownership/confirm` - to complete verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` (and a `token` - for embedding); the user completes a hosted biometric flow and - verification completes asynchronously. Status transitions are delivered - via `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_*` webhooks and reflected in - the account's `ownershipVerificationStatus`. - - This endpoint is only meaningful for accounts whose - `ownershipVerificationStatus` is `REQUIRED` or `FAILED`. For other accounts, this returns `409`. - operationId: verifyPlatformExternalAccountOwnership - tags: - - External Accounts - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/external_accounts/OwnershipVerificationStartRequest.yaml - responses: - '200': - description: Ownership verification started; the method-specific material is returned. - content: - application/json: - schema: - $ref: ../../components/schemas/external_accounts/OwnershipVerificationStart.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: External account not found - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error404.yaml - '409': - description: Ownership verification is not applicable to this external account. - 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/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml deleted file mode 100644 index 48b04e11f..000000000 --- a/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify-ownership_confirm.yaml +++ /dev/null @@ -1,70 +0,0 @@ -parameters: - - name: externalAccountId - in: path - description: The unique identifier of the platform external account (self-custody crypto wallet) whose ownership is being verified. - required: true - schema: - type: string -post: - summary: Confirm platform external account ownership verification - description: | - Complete a `WALLET_SIGNATURE` ownership verification by submitting the - signature the wallet produced for the `messageToSign` returned by - `POST /platform/external-accounts/{externalAccountId}/verify-ownership`. - The message must be signed exactly as returned, and the signature must be - submitted before the session's `expiresAt`; after expiry, start a new - verification. - - Returns the updated external account, including its new - `ownershipVerificationStatus`. - - This endpoint is only meaningful for accounts with an in-progress - `WALLET_SIGNATURE` verification. For other accounts, this returns `409`. - operationId: confirmPlatformExternalAccountOwnershipVerification - tags: - - External Accounts - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/external_accounts/OwnershipVerificationConfirmRequest.yaml - responses: - '200': - description: Ownership verification submitted; the updated external account is returned. - content: - application/json: - schema: - $ref: ../../components/schemas/external_accounts/ExternalAccount.yaml - '400': - description: Invalid or expired signature - 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: External account not found - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error404.yaml - '409': - description: Ownership verification is not applicable to this external account. - 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/webhooks/external-account-status.yaml b/openapi/webhooks/external-account-status.yaml new file mode 100644 index 000000000..b8b02ab0d --- /dev/null +++ b/openapi/webhooks/external-account-status.yaml @@ -0,0 +1,86 @@ +post: + summary: External account status webhook + description: > + Webhook that is called when the status of an external account changes + (e.g., `PENDING_OWNERSHIP_VERIFICATION` → `ACTIVE` after ownership + verification completes). + + This endpoint should be implemented by clients of the Grid API. + + + ### Authentication + + The webhook includes a signature in the `X-Grid-Signature` header that + allows you to verify that the webhook was sent by Grid. + + To verify the signature: + + 1. Get the Grid public key provided to you during integration + + 2. Decode the base64 signature from the header + + 3. Create a SHA-256 hash of the request body + + 4. Verify the signature using the public key and the hash + + + If the signature verification succeeds, the webhook is authentic. If not, it + should be rejected. + + + ### Event types + + - `EXTERNAL_ACCOUNT.STATUS_UPDATED` — Fired when the status of an external + account changes. The `data` payload contains the full external account + object. + + + operationId: externalAccountStatusWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/webhooks/ExternalAccountStatusWebhook.yaml + examples: + statusUpdated: + summary: A wallet account became active after ownership verification + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000042 + type: EXTERNAL_ACCOUNT.STATUS_UPDATED + timestamp: '2025-08-15T14:32:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: ACTIVE + currency: USDC + ownershipType: FIRST_PARTY + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + responses: + '200': + description: > + Webhook received successfully + '400': + description: Bad request + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized - Signature validation failed + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error401.yaml + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error409.yaml diff --git a/openapi/webhooks/external-account.yaml b/openapi/webhooks/external-account.yaml deleted file mode 100644 index 510251415..000000000 --- a/openapi/webhooks/external-account.yaml +++ /dev/null @@ -1,119 +0,0 @@ -post: - summary: External account ownership verification status change - description: > - Webhook that is called when the ownership verification status of an - external account 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. - - - ### Event types - - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_NOT_REQUIRED` — Fired when an - external account is created and ownership verification does not apply to - it. The `data` payload contains the full external account object. - - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_REQUIRED` — Fired when an - external account requires ownership verification before transfers above - regulatory thresholds can be sent to it. The `data` payload contains the - full external account object. - - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_PENDING_REVIEW` — Fired when a - submitted ownership verification enters review. The `data` payload contains - the full external account object. - - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED` — Fired when ownership - of the external account has been verified. The `data` payload contains the - full external account object. - - - `EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED` — Fired when an - ownership verification attempt fails; a new verification can be started. - The `data` payload contains the full external account object. - - - operationId: externalAccountWebhook - tags: - - Webhooks - security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../components/schemas/webhooks/ExternalAccountWebhook.yaml - examples: - ownershipVerified: - summary: Ownership of a self-custody wallet has been verified - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000040 - type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_VERIFIED - timestamp: '2025-08-15T14:32:00Z' - data: - id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 - status: ACTIVE - currency: USDC - ownershipType: FIRST_PARTY - ownershipVerificationStatus: VERIFIED - accountInfo: - accountType: ETHEREUM_WALLET - address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' - ownershipVerificationFailed: - summary: An ownership verification attempt failed - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000041 - type: EXTERNAL_ACCOUNT.OWNERSHIP_VERIFICATION_FAILED - timestamp: '2025-08-15T14:32:00Z' - data: - id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 - customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 - status: ACTIVE - currency: USDC - ownershipType: FIRST_PARTY - ownershipVerificationStatus: FAILED - accountInfo: - accountType: ETHEREUM_WALLET - address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' - responses: - '200': - description: > - Webhook received successfully - '400': - description: Bad request - content: - application/json: - schema: - $ref: ../components/schemas/errors/Error400.yaml - '401': - description: Unauthorized - Signature validation failed - content: - application/json: - schema: - $ref: ../components/schemas/errors/Error401.yaml - '409': - description: Conflict - Webhook has already been processed (duplicate id) - content: - application/json: - schema: - $ref: ../components/schemas/errors/Error409.yaml diff --git a/openapi/webhooks/ownership-verification.yaml b/openapi/webhooks/ownership-verification.yaml new file mode 100644 index 000000000..1726feb5e --- /dev/null +++ b/openapi/webhooks/ownership-verification.yaml @@ -0,0 +1,109 @@ +post: + summary: Ownership verification status change + description: > + Webhook that is called when the status of an ownership verification + 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. + + + ### Event types + + - `OWNERSHIP_VERIFICATION.PENDING_REVIEW` — Fired when a submitted + ownership verification enters review. The `data` payload contains the full + ownership verification object. + + - `OWNERSHIP_VERIFICATION.VERIFIED` — Fired when ownership of the external + account has been verified. The `data` payload contains the full ownership + verification object. + + - `OWNERSHIP_VERIFICATION.FAILED` — Fired when an ownership verification + attempt fails; start a new verification to retry. The `data` payload + contains the full ownership verification object. + + + operationId: ownershipVerificationWebhook + tags: + - Webhooks + security: + - WebhookSignature: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/webhooks/OwnershipVerificationWebhook.yaml + examples: + verified: + summary: Ownership of a self-custody wallet has been verified + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000040 + type: OWNERSHIP_VERIFICATION.VERIFIED + timestamp: '2025-08-15T14:32:00Z' + data: + id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 + externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: WALLET_SIGNATURE + status: VERIFIED + messageToSign: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + expiresAt: '2025-08-15T15:32:00Z' + createdAt: '2025-08-15T15:02:00Z' + updatedAt: '2025-08-15T14:32:00Z' + failed: + summary: An ownership verification attempt failed + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000041 + type: OWNERSHIP_VERIFICATION.FAILED + timestamp: '2025-08-15T14:32:00Z' + data: + id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 + externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 + method: LIVENESS + status: FAILED + verificationLink: https://verify.example.com/session/019542f5-b3e7-1d02 + token: eyJhbGciOiJIUzI1NiJ9.example + expiresAt: '2025-08-15T15:32:00Z' + createdAt: '2025-08-15T15:02:00Z' + updatedAt: '2025-08-15T14:32:00Z' + responses: + '200': + description: > + Webhook received successfully + '400': + description: Bad request + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized - Signature validation failed + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error401.yaml + '409': + description: Conflict - Webhook has already been processed (duplicate id) + content: + application/json: + schema: + $ref: ../components/schemas/errors/Error409.yaml From 39798cfba898b4cb1dbbd922b0526533a6c5046c Mon Sep 17 00:00:00 2001 From: shreyav Date: Thu, 6 Aug 2026 08:36:06 -0700 Subject: [PATCH 04/11] Address review: error-code cleanup, all-status webhook wording, Stainless resource - Revert AMOUNT_OUT_OF_RANGE description to its original text - Rename WALLET_VERIFICATION_REQUIRED to EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED - Document EXTERNAL_ACCOUNT.STATUS_UPDATED as firing on every status transition, not only ownership verification - Register the ownership_verifications resource in .stainless/stainless.yml so the endpoints reach the documented spec and SDKs Co-Authored-By: Claude Fable 5 --- .stainless/stainless.yml | 11 +++++++++++ mintlify/openapi.yaml | 8 ++++---- openapi.yaml | 8 ++++---- openapi/components/schemas/errors/Error400.yaml | 6 +++--- openapi/webhooks/external-account-status.yaml | 7 ++++--- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index 49100e04c..39dde036f 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -533,6 +533,17 @@ resources: submit: post /verifications list: get /verifications retrieve: get /verifications/{verificationId} + ownership_verifications: + models: + ownership_verification: '#/components/schemas/OwnershipVerification' + ownership_verification_list_response: '#/components/schemas/OwnershipVerificationListResponse' + ownership_verification_request: '#/components/schemas/OwnershipVerificationRequest' + ownership_verification_confirm_request: '#/components/schemas/OwnershipVerificationConfirmRequest' + methods: + create: post /ownership-verifications + list: get /ownership-verifications + retrieve: get /ownership-verifications/{verificationId} + confirm: post /ownership-verifications/{verificationId}/confirm discoveries: models: discovery_list_response: '#/components/schemas/DiscoveryListResponse' diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index d8ba938a3..e4cc71371 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -10998,7 +10998,7 @@ webhooks: post: summary: External account status webhook description: | - Webhook that is called when the status of an external account changes (e.g., `PENDING_OWNERSHIP_VERIFICATION` → `ACTIVE` after ownership verification completes). + Webhook that is called whenever the status of an external account changes, for any transition between statuses (`PENDING`, `ACTIVE`, `UNDER_REVIEW`, `INACTIVE`, `PENDING_OWNERSHIP_VERIFICATION`) — for example when an account under review becomes active, or when ownership verification completes. This endpoint should be implemented by clients of the Grid API. ### Authentication @@ -12212,7 +12212,7 @@ components: | INVALID_PUBKEY_FORMAT | Counterparty Public key format is invalid | | MISSING_REQUIRED_UMA_PARAMETERS | Counterparty required UMA parameters are missing | | SENDER_NOT_ACCEPTED | Sender is not accepted | - | AMOUNT_OUT_OF_RANGE | Amount is out of range for the quote or the destination account | + | AMOUNT_OUT_OF_RANGE | Amount is out of range | | INVALID_CURRENCY | Currency is invalid | | INVALID_TIMESTAMP | Timestamp is invalid | | INVALID_NONCE | Nonce is invalid | @@ -12236,7 +12236,7 @@ components: | STABLECOIN_PROVIDER_ACCOUNT_INVALID | The stablecoin provider account link is not usable | | STABLECOIN_PROVIDER_ACCOUNT_REVOKED | The stablecoin provider account link has been revoked | | STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED | Multiple active provider account links exist; pass `stablecoinProviderAccountId` to select one | - | WALLET_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | + | EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | enum: - INVALID_INPUT - END_USER_TERMS_VERSION_NOT_FOUND @@ -12278,7 +12278,7 @@ components: - STABLECOIN_PROVIDER_ACCOUNT_INVALID - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED - - WALLET_VERIFICATION_REQUIRED + - EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED message: type: string description: Error message diff --git a/openapi.yaml b/openapi.yaml index d8ba938a3..e4cc71371 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -10998,7 +10998,7 @@ webhooks: post: summary: External account status webhook description: | - Webhook that is called when the status of an external account changes (e.g., `PENDING_OWNERSHIP_VERIFICATION` → `ACTIVE` after ownership verification completes). + Webhook that is called whenever the status of an external account changes, for any transition between statuses (`PENDING`, `ACTIVE`, `UNDER_REVIEW`, `INACTIVE`, `PENDING_OWNERSHIP_VERIFICATION`) — for example when an account under review becomes active, or when ownership verification completes. This endpoint should be implemented by clients of the Grid API. ### Authentication @@ -12212,7 +12212,7 @@ components: | INVALID_PUBKEY_FORMAT | Counterparty Public key format is invalid | | MISSING_REQUIRED_UMA_PARAMETERS | Counterparty required UMA parameters are missing | | SENDER_NOT_ACCEPTED | Sender is not accepted | - | AMOUNT_OUT_OF_RANGE | Amount is out of range for the quote or the destination account | + | AMOUNT_OUT_OF_RANGE | Amount is out of range | | INVALID_CURRENCY | Currency is invalid | | INVALID_TIMESTAMP | Timestamp is invalid | | INVALID_NONCE | Nonce is invalid | @@ -12236,7 +12236,7 @@ components: | STABLECOIN_PROVIDER_ACCOUNT_INVALID | The stablecoin provider account link is not usable | | STABLECOIN_PROVIDER_ACCOUNT_REVOKED | The stablecoin provider account link has been revoked | | STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED | Multiple active provider account links exist; pass `stablecoinProviderAccountId` to select one | - | WALLET_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | + | EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | enum: - INVALID_INPUT - END_USER_TERMS_VERSION_NOT_FOUND @@ -12278,7 +12278,7 @@ components: - STABLECOIN_PROVIDER_ACCOUNT_INVALID - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED - - WALLET_VERIFICATION_REQUIRED + - EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED message: type: string description: Error message diff --git a/openapi/components/schemas/errors/Error400.yaml b/openapi/components/schemas/errors/Error400.yaml index 6185df648..87ba0ce6a 100644 --- a/openapi/components/schemas/errors/Error400.yaml +++ b/openapi/components/schemas/errors/Error400.yaml @@ -30,7 +30,7 @@ properties: | INVALID_PUBKEY_FORMAT | Counterparty Public key format is invalid | | MISSING_REQUIRED_UMA_PARAMETERS | Counterparty required UMA parameters are missing | | SENDER_NOT_ACCEPTED | Sender is not accepted | - | AMOUNT_OUT_OF_RANGE | Amount is out of range for the quote or the destination account | + | AMOUNT_OUT_OF_RANGE | Amount is out of range | | INVALID_CURRENCY | Currency is invalid | | INVALID_TIMESTAMP | Timestamp is invalid | | INVALID_NONCE | Nonce is invalid | @@ -54,7 +54,7 @@ properties: | STABLECOIN_PROVIDER_ACCOUNT_INVALID | The stablecoin provider account link is not usable | | STABLECOIN_PROVIDER_ACCOUNT_REVOKED | The stablecoin provider account link has been revoked | | STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED | Multiple active provider account links exist; pass `stablecoinProviderAccountId` to select one | - | WALLET_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | + | EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED | The destination account's ownership must be verified before this transfer can proceed | enum: - INVALID_INPUT - END_USER_TERMS_VERSION_NOT_FOUND @@ -96,7 +96,7 @@ properties: - STABLECOIN_PROVIDER_ACCOUNT_INVALID - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED - - WALLET_VERIFICATION_REQUIRED + - EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED message: type: string description: Error message diff --git a/openapi/webhooks/external-account-status.yaml b/openapi/webhooks/external-account-status.yaml index b8b02ab0d..cb53af481 100644 --- a/openapi/webhooks/external-account-status.yaml +++ b/openapi/webhooks/external-account-status.yaml @@ -1,9 +1,10 @@ post: summary: External account status webhook description: > - Webhook that is called when the status of an external account changes - (e.g., `PENDING_OWNERSHIP_VERIFICATION` → `ACTIVE` after ownership - verification completes). + Webhook that is called whenever the status of an external account changes, + for any transition between statuses (`PENDING`, `ACTIVE`, `UNDER_REVIEW`, + `INACTIVE`, `PENDING_OWNERSHIP_VERIFICATION`) — for example when an account + under review becomes active, or when ownership verification completes. This endpoint should be implemented by clients of the Grid API. From 207bd7961330dad66db7ec4c3c84220bc16caa74 Mon Sep 17 00:00:00 2001 From: shreyav Date: Tue, 11 Aug 2026 15:28:33 +0000 Subject: [PATCH 05/11] Add shield sidebar icon for Ownership Verifications docs group --- mintlify/style.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mintlify/style.css b/mintlify/style.css index 0131dfb69..7d2b5167d 100644 --- a/mintlify/style.css +++ b/mintlify/style.css @@ -1330,6 +1330,12 @@ ul.sidebar-group > li[data-title="External Accounts"] > button::before { background-image: url('/images/icons/bank.svg') !important; } +/* Ownership Verifications - shield */ +.sidebar-group > li[data-title="Ownership Verifications"] > button::before, +ul.sidebar-group > li[data-title="Ownership Verifications"] > button::before { + background-image: url('/images/icons/shield.svg') !important; +} + /* Same-Currency Transfers - horizontal expand arrows */ .sidebar-group > li[data-title="Same-Currency Transfers"] > button::before, ul.sidebar-group > li[data-title="Same-Currency Transfers"] > button::before { From a9cbf4462b34696464683f2eeb7be8ab948394bd Mon Sep 17 00:00:00 2001 From: shreyav Date: Tue, 11 Aug 2026 15:33:15 +0000 Subject: [PATCH 06/11] Make liveness verification token optional, matching the KYC link contract --- mintlify/openapi.yaml | 3 +-- openapi.yaml | 3 +-- .../LivenessOwnershipVerification.yaml | 8 +++++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 25a601060..4b8a317e1 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -20654,7 +20654,6 @@ components: - method - status - verificationLink - - token - expiresAt - createdAt properties: @@ -20681,7 +20680,7 @@ components: example: https://verify.example.com/session/019542f5-b3e7-1d02 token: type: string - description: Access token for embedding the verification flow in the platform's own UI, as an alternative to `verificationLink`. + description: Provider-specific token that can be used in place of `verificationLink` — 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 verification outcome. example: eyJhbGciOiJIUzI1NiJ9.example expiresAt: type: string diff --git a/openapi.yaml b/openapi.yaml index 25a601060..4b8a317e1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -20654,7 +20654,6 @@ components: - method - status - verificationLink - - token - expiresAt - createdAt properties: @@ -20681,7 +20680,7 @@ components: example: https://verify.example.com/session/019542f5-b3e7-1d02 token: type: string - description: Access token for embedding the verification flow in the platform's own UI, as an alternative to `verificationLink`. + description: Provider-specific token that can be used in place of `verificationLink` — 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 verification outcome. example: eyJhbGciOiJIUzI1NiJ9.example expiresAt: type: string diff --git a/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml b/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml index cbb4db8a5..b565fc23c 100644 --- a/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml +++ b/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml @@ -9,7 +9,6 @@ required: - method - status - verificationLink - - token - expiresAt - createdAt properties: @@ -37,8 +36,11 @@ properties: token: type: string description: >- - Access token for embedding the verification flow in the platform's own - UI, as an alternative to `verificationLink`. + Provider-specific token that can be used in place of `verificationLink` — + 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 verification outcome. example: eyJhbGciOiJIUzI1NiJ9.example expiresAt: type: string From bb910f8686d3f390c1dc07b633c042395565d6d1 Mon Sep 17 00:00:00 2001 From: shreyav Date: Tue, 11 Aug 2026 15:55:17 +0000 Subject: [PATCH 07/11] Fix liveness token wording (review feedback) and webhook example timestamps --- mintlify/openapi.yaml | 12 ++++++------ openapi.yaml | 12 ++++++------ .../ownership-verifications.yaml | 4 ++-- openapi/webhooks/ownership-verification.yaml | 8 ++++---- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 4b8a317e1..90eacd2e1 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -2646,8 +2646,8 @@ paths: wallet sign it and submit the result to `POST /ownership-verifications/{verificationId}/confirm` to complete verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` (and a `token` - for embedding); the user completes a hosted biometric flow and + - `LIVENESS` — the response includes a `verificationLink` and may include a + `token` for embedding; the user completes a hosted biometric flow and verification completes asynchronously. Status transitions are delivered via `OWNERSHIP_VERIFICATION.*` webhooks or by polling `GET /ownership-verifications/{verificationId}`. @@ -11179,7 +11179,7 @@ webhooks: value: id: Webhook:019542f5-b3e7-1d02-0000-000000000040 type: OWNERSHIP_VERIFICATION.VERIFIED - timestamp: '2025-08-15T14:32:00Z' + timestamp: '2025-08-15T15:10:00Z' data: id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 @@ -11188,13 +11188,13 @@ webhooks: messageToSign: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' expiresAt: '2025-08-15T15:32:00Z' createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T14:32:00Z' + updatedAt: '2025-08-15T15:10:00Z' failed: summary: An ownership verification attempt failed value: id: Webhook:019542f5-b3e7-1d02-0000-000000000041 type: OWNERSHIP_VERIFICATION.FAILED - timestamp: '2025-08-15T14:32:00Z' + timestamp: '2025-08-15T15:10:00Z' data: id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 @@ -11204,7 +11204,7 @@ webhooks: token: eyJhbGciOiJIUzI1NiJ9.example expiresAt: '2025-08-15T15:32:00Z' createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T14:32:00Z' + updatedAt: '2025-08-15T15:10:00Z' responses: '200': description: | diff --git a/openapi.yaml b/openapi.yaml index 4b8a317e1..90eacd2e1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2646,8 +2646,8 @@ paths: wallet sign it and submit the result to `POST /ownership-verifications/{verificationId}/confirm` to complete verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` (and a `token` - for embedding); the user completes a hosted biometric flow and + - `LIVENESS` — the response includes a `verificationLink` and may include a + `token` for embedding; the user completes a hosted biometric flow and verification completes asynchronously. Status transitions are delivered via `OWNERSHIP_VERIFICATION.*` webhooks or by polling `GET /ownership-verifications/{verificationId}`. @@ -11179,7 +11179,7 @@ webhooks: value: id: Webhook:019542f5-b3e7-1d02-0000-000000000040 type: OWNERSHIP_VERIFICATION.VERIFIED - timestamp: '2025-08-15T14:32:00Z' + timestamp: '2025-08-15T15:10:00Z' data: id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 @@ -11188,13 +11188,13 @@ webhooks: messageToSign: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' expiresAt: '2025-08-15T15:32:00Z' createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T14:32:00Z' + updatedAt: '2025-08-15T15:10:00Z' failed: summary: An ownership verification attempt failed value: id: Webhook:019542f5-b3e7-1d02-0000-000000000041 type: OWNERSHIP_VERIFICATION.FAILED - timestamp: '2025-08-15T14:32:00Z' + timestamp: '2025-08-15T15:10:00Z' data: id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 @@ -11204,7 +11204,7 @@ webhooks: token: eyJhbGciOiJIUzI1NiJ9.example expiresAt: '2025-08-15T15:32:00Z' createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T14:32:00Z' + updatedAt: '2025-08-15T15:10:00Z' responses: '200': description: | diff --git a/openapi/paths/ownership_verifications/ownership-verifications.yaml b/openapi/paths/ownership_verifications/ownership-verifications.yaml index fe0b2f100..3625a972d 100644 --- a/openapi/paths/ownership_verifications/ownership-verifications.yaml +++ b/openapi/paths/ownership_verifications/ownership-verifications.yaml @@ -8,8 +8,8 @@ post: wallet sign it and submit the result to `POST /ownership-verifications/{verificationId}/confirm` to complete verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` (and a `token` - for embedding); the user completes a hosted biometric flow and + - `LIVENESS` — the response includes a `verificationLink` and may include a + `token` for embedding; the user completes a hosted biometric flow and verification completes asynchronously. Status transitions are delivered via `OWNERSHIP_VERIFICATION.*` webhooks or by polling `GET /ownership-verifications/{verificationId}`. diff --git a/openapi/webhooks/ownership-verification.yaml b/openapi/webhooks/ownership-verification.yaml index 1726feb5e..4a507f139 100644 --- a/openapi/webhooks/ownership-verification.yaml +++ b/openapi/webhooks/ownership-verification.yaml @@ -59,7 +59,7 @@ post: value: id: Webhook:019542f5-b3e7-1d02-0000-000000000040 type: OWNERSHIP_VERIFICATION.VERIFIED - timestamp: '2025-08-15T14:32:00Z' + timestamp: '2025-08-15T15:10:00Z' data: id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 @@ -68,13 +68,13 @@ post: messageToSign: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' expiresAt: '2025-08-15T15:32:00Z' createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T14:32:00Z' + updatedAt: '2025-08-15T15:10:00Z' failed: summary: An ownership verification attempt failed value: id: Webhook:019542f5-b3e7-1d02-0000-000000000041 type: OWNERSHIP_VERIFICATION.FAILED - timestamp: '2025-08-15T14:32:00Z' + timestamp: '2025-08-15T15:10:00Z' data: id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 @@ -84,7 +84,7 @@ post: token: eyJhbGciOiJIUzI1NiJ9.example expiresAt: '2025-08-15T15:32:00Z' createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T14:32:00Z' + updatedAt: '2025-08-15T15:10:00Z' responses: '200': description: > From 7b12a8be70492095fff8d0aa9fca4eb0593a524b Mon Sep 17 00:00:00 2001 From: shreyav Date: Thu, 13 Aug 2026 18:21:22 +0000 Subject: [PATCH 08/11] Generalize PENDING_OWNERSHIP_VERIFICATION docs beyond EU Travel Rule --- mintlify/openapi.yaml | 13 +++++++------ openapi.yaml | 13 +++++++------ .../external_accounts/ExternalAccountStatus.yaml | 13 +++++++------ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 90eacd2e1..4d01267fa 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -17256,12 +17256,13 @@ components: description: | Status of an external account. - `PENDING_OWNERSHIP_VERIFICATION` applies to `FIRST_PARTY` self-custody - crypto wallet accounts on platforms subject to EU Travel Rule requirements. - While in this status, the account can be used for transfers below regulatory - thresholds; completing ownership verification (see the Ownership - Verifications API) moves the account to `ACTIVE` and removes the - restriction. + `PENDING_OWNERSHIP_VERIFICATION` applies to crypto wallet accounts whose + ownership must be verified before the account can be used without + restriction — for example, under the EU Travel Rule or similar requirements + in other regions. While in this status, the account can be used for + transfers below regulatory thresholds; completing ownership verification + (see the Ownership Verifications API) moves the account to `ACTIVE` and + removes the restriction. OwnershipType: type: string enum: diff --git a/openapi.yaml b/openapi.yaml index 90eacd2e1..4d01267fa 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -17256,12 +17256,13 @@ components: description: | Status of an external account. - `PENDING_OWNERSHIP_VERIFICATION` applies to `FIRST_PARTY` self-custody - crypto wallet accounts on platforms subject to EU Travel Rule requirements. - While in this status, the account can be used for transfers below regulatory - thresholds; completing ownership verification (see the Ownership - Verifications API) moves the account to `ACTIVE` and removes the - restriction. + `PENDING_OWNERSHIP_VERIFICATION` applies to crypto wallet accounts whose + ownership must be verified before the account can be used without + restriction — for example, under the EU Travel Rule or similar requirements + in other regions. While in this status, the account can be used for + transfers below regulatory thresholds; completing ownership verification + (see the Ownership Verifications API) moves the account to `ACTIVE` and + removes the restriction. OwnershipType: type: string enum: diff --git a/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml b/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml index 5b62fd215..c16c9513e 100644 --- a/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml +++ b/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml @@ -8,9 +8,10 @@ enum: description: | Status of an external account. - `PENDING_OWNERSHIP_VERIFICATION` applies to `FIRST_PARTY` self-custody - crypto wallet accounts on platforms subject to EU Travel Rule requirements. - While in this status, the account can be used for transfers below regulatory - thresholds; completing ownership verification (see the Ownership - Verifications API) moves the account to `ACTIVE` and removes the - restriction. + `PENDING_OWNERSHIP_VERIFICATION` applies to crypto wallet accounts whose + ownership must be verified before the account can be used without + restriction — for example, under the EU Travel Rule or similar requirements + in other regions. While in this status, the account can be used for + transfers below regulatory thresholds; completing ownership verification + (see the Ownership Verifications API) moves the account to `ACTIVE` and + removes the restriction. From 931e0ad6987abe604f6d446c9e6f52454801d953 Mon Sep 17 00:00:00 2001 From: shreyav Date: Thu, 13 Aug 2026 18:22:57 +0000 Subject: [PATCH 09/11] Generalize ownershipType requirement wording beyond EU Travel Rule --- mintlify/openapi.yaml | 2 +- openapi.yaml | 2 +- .../schemas/external_accounts/OwnershipType.yaml | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 4d01267fa..6f721dd17 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -17268,7 +17268,7 @@ components: enum: - FIRST_PARTY - THIRD_PARTY - description: Whether the external account belongs to the customer themselves (`FIRST_PARTY`) or to someone else (`THIRD_PARTY`). Required when creating self-custody crypto wallet external accounts on platforms subject to EU Travel Rule requirements; recommended for all other accounts, where providing it can unlock additional capabilities and smoother compliance handling. + description: Whether the external account belongs to the customer themselves (`FIRST_PARTY`) or to someone else (`THIRD_PARTY`). Required when creating self-custody crypto wallet external accounts on platforms subject to counterparty requirements — for example, under the EU Travel Rule or similar requirements in other regions; recommended for all other accounts, where providing it can unlock additional capabilities and smoother compliance handling. example: FIRST_PARTY BeneficiaryVerificationStatus: type: string diff --git a/openapi.yaml b/openapi.yaml index 4d01267fa..6f721dd17 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -17268,7 +17268,7 @@ components: enum: - FIRST_PARTY - THIRD_PARTY - description: Whether the external account belongs to the customer themselves (`FIRST_PARTY`) or to someone else (`THIRD_PARTY`). Required when creating self-custody crypto wallet external accounts on platforms subject to EU Travel Rule requirements; recommended for all other accounts, where providing it can unlock additional capabilities and smoother compliance handling. + description: Whether the external account belongs to the customer themselves (`FIRST_PARTY`) or to someone else (`THIRD_PARTY`). Required when creating self-custody crypto wallet external accounts on platforms subject to counterparty requirements — for example, under the EU Travel Rule or similar requirements in other regions; recommended for all other accounts, where providing it can unlock additional capabilities and smoother compliance handling. example: FIRST_PARTY BeneficiaryVerificationStatus: type: string diff --git a/openapi/components/schemas/external_accounts/OwnershipType.yaml b/openapi/components/schemas/external_accounts/OwnershipType.yaml index 6f369994e..41be40af2 100644 --- a/openapi/components/schemas/external_accounts/OwnershipType.yaml +++ b/openapi/components/schemas/external_accounts/OwnershipType.yaml @@ -5,8 +5,9 @@ enum: description: >- Whether the external account belongs to the customer themselves (`FIRST_PARTY`) or to someone else (`THIRD_PARTY`). Required when creating - self-custody crypto wallet external accounts on platforms subject to EU - Travel Rule requirements; recommended for all other accounts, where - providing it can unlock additional capabilities and smoother compliance - handling. + self-custody crypto wallet external accounts on platforms subject to + counterparty requirements — for example, under the EU Travel Rule or + similar requirements in other regions; recommended for all other accounts, + where providing it can unlock additional capabilities and smoother + compliance handling. example: FIRST_PARTY From 906c60310ed6aba9b2061c27e71c49147fab495a Mon Sep 17 00:00:00 2001 From: shreyav Date: Thu, 13 Aug 2026 23:24:39 +0000 Subject: [PATCH 10/11] Rework ownership verification into account-scoped challenge/verify endpoints Replaces the standalone /ownership-verifications resource with challenge/verify actions on the external account (customer and platform trees). Verification state lives on the account: a new UNVERIFIED status marks a failed attempt, so EXTERNAL_ACCOUNT.STATUS_UPDATED carries the whole lifecycle and the OWNERSHIP_VERIFICATION.* webhooks are removed. --- .stainless/stainless.yml | 20 +- mintlify/openapi.yaml | 689 +++++++----------- mintlify/style.css | 6 - openapi.yaml | 689 +++++++----------- .../ExternalAccountStatus.yaml | 15 +- .../external_accounts/LivenessChallenge.yaml | 39 + .../external_accounts/OwnershipChallenge.yaml | 11 + .../OwnershipChallengeRequest.yaml | 10 + .../OwnershipVerificationMethod.yaml | 0 .../OwnershipVerifyRequest.yaml} | 9 +- .../WalletSignatureChallenge.yaml | 29 + .../LivenessOwnershipVerification.yaml | 61 -- .../OwnershipVerification.yaml | 11 - .../OwnershipVerificationListResponse.yaml | 21 - .../OwnershipVerificationRequest.yaml | 17 - .../OwnershipVerificationState.yaml | 16 - .../WalletSignatureOwnershipVerification.yaml | 51 -- .../OwnershipVerificationWebhook.yaml | 14 - .../schemas/webhooks/WebhookType.yaml | 3 - openapi/openapi.yaml | 20 +- ...ccounts_{externalAccountId}_challenge.yaml | 81 ++ ...l_accounts_{externalAccountId}_verify.yaml | 74 ++ .../ownership-verifications.yaml | 133 ---- ...ership-verifications_{verificationId}.yaml | 40 - ...erifications_{verificationId}_confirm.yaml | 72 -- ...ccounts_{externalAccountId}_challenge.yaml | 81 ++ ...l_accounts_{externalAccountId}_verify.yaml | 74 ++ openapi/webhooks/external-account-status.yaml | 24 +- openapi/webhooks/ownership-verification.yaml | 109 --- 29 files changed, 1014 insertions(+), 1405 deletions(-) create mode 100644 openapi/components/schemas/external_accounts/LivenessChallenge.yaml create mode 100644 openapi/components/schemas/external_accounts/OwnershipChallenge.yaml create mode 100644 openapi/components/schemas/external_accounts/OwnershipChallengeRequest.yaml rename openapi/components/schemas/{ownership_verifications => external_accounts}/OwnershipVerificationMethod.yaml (100%) rename openapi/components/schemas/{ownership_verifications/OwnershipVerificationConfirmRequest.yaml => external_accounts/OwnershipVerifyRequest.yaml} (65%) create mode 100644 openapi/components/schemas/external_accounts/WalletSignatureChallenge.yaml delete mode 100644 openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml delete mode 100644 openapi/components/schemas/ownership_verifications/OwnershipVerification.yaml delete mode 100644 openapi/components/schemas/ownership_verifications/OwnershipVerificationListResponse.yaml delete mode 100644 openapi/components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml delete mode 100644 openapi/components/schemas/ownership_verifications/OwnershipVerificationState.yaml delete mode 100644 openapi/components/schemas/ownership_verifications/WalletSignatureOwnershipVerification.yaml delete mode 100644 openapi/components/schemas/webhooks/OwnershipVerificationWebhook.yaml create mode 100644 openapi/paths/customers/customers_external_accounts_{externalAccountId}_challenge.yaml create mode 100644 openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify.yaml delete mode 100644 openapi/paths/ownership_verifications/ownership-verifications.yaml delete mode 100644 openapi/paths/ownership_verifications/ownership-verifications_{verificationId}.yaml delete mode 100644 openapi/paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml create mode 100644 openapi/paths/platform/platform_external_accounts_{externalAccountId}_challenge.yaml create mode 100644 openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify.yaml delete mode 100644 openapi/webhooks/ownership-verification.yaml diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index 39dde036f..781afc91c 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -210,6 +210,11 @@ resources: external_account_create: '#/components/schemas/ExternalAccountCreateRequest' external_account_info_one_of: "#/components/schemas/ExternalAccountInfoOneOf" business_beneficiary: "#/components/schemas/BusinessBeneficiary" + # Ownership verification (challenge/verify) + ownership_verification_method: "#/components/schemas/OwnershipVerificationMethod" + ownership_challenge_request: "#/components/schemas/OwnershipChallengeRequest" + ownership_challenge: "#/components/schemas/OwnershipChallenge" + ownership_verify_request: "#/components/schemas/OwnershipVerifyRequest" address: "#/components/schemas/Address" beneficiary_verified_data: "#/components/schemas/BeneficiaryVerifiedData" # List response @@ -219,6 +224,8 @@ resources: create: post /customers/external-accounts retrieve: get /customers/external-accounts/{externalAccountId} delete: delete /customers/external-accounts/{externalAccountId} + challenge: post /customers/external-accounts/{externalAccountId}/challenge + verify: post /customers/external-accounts/{externalAccountId}/verify bulk: methods: upload_csv: post /customers/bulk/csv @@ -238,6 +245,8 @@ resources: create: post /platform/external-accounts retrieve: get /platform/external-accounts/{externalAccountId} delete: delete /platform/external-accounts/{externalAccountId} + challenge: post /platform/external-accounts/{externalAccountId}/challenge + verify: post /platform/external-accounts/{externalAccountId}/verify models: platform_external_account_create_request: "#/components/schemas/PlatformExternalAccountCreateRequest" aed_account_info: "#/components/schemas/AedAccountInfo" @@ -533,17 +542,6 @@ resources: submit: post /verifications list: get /verifications retrieve: get /verifications/{verificationId} - ownership_verifications: - models: - ownership_verification: '#/components/schemas/OwnershipVerification' - ownership_verification_list_response: '#/components/schemas/OwnershipVerificationListResponse' - ownership_verification_request: '#/components/schemas/OwnershipVerificationRequest' - ownership_verification_confirm_request: '#/components/schemas/OwnershipVerificationConfirmRequest' - methods: - create: post /ownership-verifications - list: get /ownership-verifications - retrieve: get /ownership-verifications/{verificationId} - confirm: post /ownership-verifications/{verificationId}/confirm discoveries: models: discovery_list_response: '#/components/schemas/DiscoveryListResponse' diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index da2b9b5b2..894642231 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -33,8 +33,6 @@ tags: description: Internal account management endpoints for creating and managing internal accounts - name: External Accounts description: External account management endpoints for creating and managing external bank accounts - - name: Ownership Verifications - description: Endpoints for verifying ownership of self-custody crypto wallet external accounts, via wallet signature or a hosted biometric verification flow. - name: Same-Currency Transfers description: Endpoints for transferring funds between internal and external accounts with the same currency - name: Cross-Currency Transfers @@ -2431,6 +2429,154 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/external-accounts/{externalAccountId}/challenge: + post: + summary: Start an ownership verification challenge + description: | + Start (or restart) ownership verification for a `FIRST_PARTY` self-custody + crypto wallet external account in `PENDING_OWNERSHIP_VERIFICATION` or + `UNVERIFIED` status. The response carries the method-specific challenge + material: + + - `WALLET_SIGNATURE` — a `messageToSign`; have the wallet sign it exactly + and submit the result to the verify endpoint to complete verification + synchronously. + - `LIVENESS` — a hosted `verificationLink` (and possibly an embed + `token`); the user completes a biometric flow and verification completes + asynchronously. The outcome is delivered via + `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account. + + Calling this endpoint again abandons any in-flight challenge and issues a + new one with the requested method — use it to retry after a failed + attempt, to replace an expired challenge, or to switch methods. An + `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a + new challenge is issued. + + Completing ownership verification moves the account to `ACTIVE`. + operationId: createExternalAccountOwnershipChallenge + tags: + - External Accounts + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipChallengeRequest' + responses: + '201': + description: Challenge created; the method-specific material is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipChallenge' + '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: External account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Ownership verification is not applicable to this external account (not a self-custody crypto wallet, not `FIRST_PARTY`, or already verified). + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /customers/external-accounts/{externalAccountId}/verify: + post: + summary: Verify ownership with a wallet signature + description: | + Complete a `WALLET_SIGNATURE` challenge by submitting the signature the + wallet produced for the challenge's `messageToSign`. The message must be + signed exactly as returned, and the signature must be submitted before + the challenge's `expiresAt` — after expiry, start a new challenge. + + On success the account moves to `ACTIVE`; on an invalid signature it + moves to `UNVERIFIED` (start a new challenge to retry). `LIVENESS` + challenges complete asynchronously and never use this endpoint — their + outcome is delivered via `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by + polling the account. + operationId: verifyExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerifyRequest' + responses: + '200': + description: 'Signature valid; the updated external account is returned with `status: ACTIVE`.' + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '400': + description: Invalid or expired signature. The account moves to `UNVERIFIED`; start a new challenge to retry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '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' + '409': + description: No `WALLET_SIGNATURE` challenge is outstanding for this external account. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /platform/external-accounts: get: summary: List platform external accounts @@ -2635,45 +2781,55 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /ownership-verifications: + /platform/external-accounts/{externalAccountId}/challenge: post: - summary: Create an ownership verification + summary: Start an ownership verification challenge description: | - Begin ownership verification for a `FIRST_PARTY` self-custody crypto - wallet external account (customer or platform owned). Choose a `method`: + Start (or restart) ownership verification for a `FIRST_PARTY` self-custody + crypto wallet external account in `PENDING_OWNERSHIP_VERIFICATION` or + `UNVERIFIED` status. The response carries the method-specific challenge + material: - - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the - wallet sign it and submit the result to - `POST /ownership-verifications/{verificationId}/confirm` to complete - verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` and may include a - `token` for embedding; the user completes a hosted biometric flow and - verification completes asynchronously. Status transitions are delivered - via `OWNERSHIP_VERIFICATION.*` webhooks or by polling - `GET /ownership-verifications/{verificationId}`. + - `WALLET_SIGNATURE` — a `messageToSign`; have the wallet sign it exactly + and submit the result to the verify endpoint to complete verification + synchronously. + - `LIVENESS` — a hosted `verificationLink` (and possibly an embed + `token`); the user completes a biometric flow and verification completes + asynchronously. The outcome is delivered via + `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account. - Ownership verification applies to accounts in - `PENDING_OWNERSHIP_VERIFICATION` status; completing it moves the account - to `ACTIVE`. For accounts where ownership verification is not applicable, - this returns `409`. - operationId: createOwnershipVerification + Calling this endpoint again abandons any in-flight challenge and issues a + new one with the requested method — use it to retry after a failed + attempt, to replace an expired challenge, or to switch methods. An + `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a + new challenge is issued. + + Completing ownership verification moves the account to `ACTIVE`. + operationId: createPlatformExternalAccountOwnershipChallenge tags: - - Ownership Verifications + - External Accounts security: - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerificationRequest' + $ref: '#/components/schemas/OwnershipChallengeRequest' responses: '201': - description: Ownership verification created; the method-specific material is returned. + description: Challenge created; the method-specific material is returned. content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerification' + $ref: '#/components/schemas/OwnershipChallenge' '400': description: Bad request - Invalid parameters content: @@ -2693,7 +2849,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: Ownership verification is not applicable to this external account. + description: Ownership verification is not applicable to this external account (not a self-custody crypto wallet, not `FIRST_PARTY`, or already verified). content: application/json: schema: @@ -2704,133 +2860,29 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - get: - summary: List ownership verifications - description: | - Retrieve a list of ownership verifications with optional filtering by external account ID and status. - operationId: listOwnershipVerifications - tags: - - Ownership Verifications - security: - - BasicAuth: [] - parameters: - - name: externalAccountId - in: query - description: Filter by external account ID - required: false - schema: - type: string - - name: status - in: query - description: Filter by verification status - required: false - schema: - $ref: '#/components/schemas/OwnershipVerificationState' - - 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/OwnershipVerificationListResponse' - '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' - /ownership-verifications/{verificationId}: - get: - summary: Get an ownership verification - description: Retrieve details of a specific ownership verification by ID. - operationId: getOwnershipVerification - tags: - - Ownership Verifications - security: - - BasicAuth: [] - parameters: - - name: verificationId - in: path - description: Ownership verification ID - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerification' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Ownership verification not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /ownership-verifications/{verificationId}/confirm: + /platform/external-accounts/{externalAccountId}/verify: post: - summary: Confirm an ownership verification + summary: Verify ownership with a wallet signature description: | - Complete a `WALLET_SIGNATURE` ownership verification by submitting the - signature the wallet produced for the `messageToSign` returned by - `POST /ownership-verifications`. The message must be signed exactly as - returned, and the signature must be submitted before the session's - `expiresAt`; after expiry, start a new verification. + Complete a `WALLET_SIGNATURE` challenge by submitting the signature the + wallet produced for the challenge's `messageToSign`. The message must be + signed exactly as returned, and the signature must be submitted before + the challenge's `expiresAt` — after expiry, start a new challenge. - This endpoint is only valid for `WALLET_SIGNATURE` verifications in - `PENDING` status. For other verifications, this returns `409`. `LIVENESS` - verifications complete asynchronously — their status is delivered via - `OWNERSHIP_VERIFICATION.*` webhooks or by polling - `GET /ownership-verifications/{verificationId}`. - operationId: confirmOwnershipVerification + On success the account moves to `ACTIVE`; on an invalid signature it + moves to `UNVERIFIED` (start a new challenge to retry). `LIVENESS` + challenges complete asynchronously and never use this endpoint — their + outcome is delivered via `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by + polling the account. + operationId: verifyPlatformExternalAccountOwnership tags: - - Ownership Verifications + - External Accounts security: - BasicAuth: [] parameters: - - name: verificationId + - name: externalAccountId in: path - description: Ownership verification ID + description: External account ID required: true schema: type: string @@ -2839,16 +2891,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' + $ref: '#/components/schemas/OwnershipVerifyRequest' responses: '200': - description: Signature submitted; the updated ownership verification is returned. + description: 'Signature valid; the updated external account is returned with `status: ACTIVE`.' content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerification' + $ref: '#/components/schemas/ExternalAccount' '400': - description: Invalid or expired signature + description: Invalid or expired signature. The account moves to `UNVERIFIED`; start a new challenge to retry. content: application/json: schema: @@ -2860,13 +2912,13 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Ownership verification not found + description: External account not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: The verification is not a `WALLET_SIGNATURE` verification in `PENDING` status. + description: No `WALLET_SIGNATURE` challenge is outstanding for this external account. content: application/json: schema: @@ -10991,7 +11043,7 @@ webhooks: post: summary: External account status webhook description: | - Webhook that is called whenever the status of an external account changes, for any transition between statuses (`PENDING`, `ACTIVE`, `UNDER_REVIEW`, `INACTIVE`, `PENDING_OWNERSHIP_VERIFICATION`) — for example when an account under review becomes active, or when ownership verification completes. + Webhook that is called whenever the status of an external account changes, for any transition between statuses (`PENDING`, `ACTIVE`, `UNDER_REVIEW`, `INACTIVE`, `PENDING_OWNERSHIP_VERIFICATION`, `UNVERIFIED`) — for example when an account under review becomes active, when ownership verification completes (`PENDING_OWNERSHIP_VERIFICATION` → `ACTIVE`), or when a verification attempt fails (`PENDING_OWNERSHIP_VERIFICATION` → `UNVERIFIED`). This endpoint should be implemented by clients of the Grid API. ### Authentication @@ -11018,7 +11070,7 @@ webhooks: schema: $ref: '#/components/schemas/ExternalAccountStatusWebhook' examples: - statusUpdated: + verificationSucceeded: summary: A wallet account became active after ownership verification value: id: Webhook:019542f5-b3e7-1d02-0000-000000000042 @@ -11033,6 +11085,21 @@ webhooks: accountInfo: accountType: ETHEREUM_WALLET address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + verificationFailed: + summary: A liveness check failed; a new challenge is needed to retry + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000043 + type: EXTERNAL_ACCOUNT.STATUS_UPDATED + timestamp: '2025-08-15T14:45:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: UNVERIFIED + currency: USDC + ownershipType: FIRST_PARTY + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' responses: '200': description: | @@ -11134,92 +11201,6 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' - ownership-verification: - post: - summary: Ownership verification status change - description: | - Webhook that is called when the status of an ownership verification 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. - - ### Event types - - `OWNERSHIP_VERIFICATION.PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full ownership verification object. - - `OWNERSHIP_VERIFICATION.VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full ownership verification object. - - `OWNERSHIP_VERIFICATION.FAILED` — Fired when an ownership verification attempt fails; start a new verification to retry. The `data` payload contains the full ownership verification object. - operationId: ownershipVerificationWebhook - tags: - - Webhooks - security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerificationWebhook' - examples: - verified: - summary: Ownership of a self-custody wallet has been verified - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000040 - type: OWNERSHIP_VERIFICATION.VERIFIED - timestamp: '2025-08-15T15:10:00Z' - data: - id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 - externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: WALLET_SIGNATURE - status: VERIFIED - messageToSign: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' - expiresAt: '2025-08-15T15:32:00Z' - createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T15:10:00Z' - failed: - summary: An ownership verification attempt failed - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000041 - type: OWNERSHIP_VERIFICATION.FAILED - timestamp: '2025-08-15T15:10:00Z' - data: - id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 - externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: LIVENESS - status: FAILED - verificationLink: https://verify.example.com/session/019542f5-b3e7-1d02 - token: eyJhbGciOiJIUzI1NiJ9.example - expiresAt: '2025-08-15T15:32:00Z' - createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T15:10:00Z' - 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' card-state-change: post: summary: Card state change @@ -17249,6 +17230,7 @@ components: - PENDING - ACTIVE - PENDING_OWNERSHIP_VERIFICATION + - UNVERIFIED - UNDER_REVIEW - INACTIVE description: | @@ -17257,10 +17239,16 @@ components: `PENDING_OWNERSHIP_VERIFICATION` applies to crypto wallet accounts whose ownership must be verified before the account can be used without restriction — for example, under the EU Travel Rule or similar requirements - in other regions. While in this status, the account can be used for - transfers below regulatory thresholds; completing ownership verification - (see the Ownership Verifications API) moves the account to `ACTIVE` and - removes the restriction. + in other regions. It covers both "verification not yet started" and + "verification in progress" (e.g. a liveness check underway). While in this + status, the account can be used for transfers below regulatory thresholds. + + `UNVERIFIED` means the most recent ownership verification attempt failed. + The account keeps the same below-threshold capabilities as + `PENDING_OWNERSHIP_VERIFICATION`; start a new challenge to retry, which + returns the account to `PENDING_OWNERSHIP_VERIFICATION`. + + Completing ownership verification moves the account to `ACTIVE`. OwnershipType: type: string enum: @@ -20560,118 +20548,67 @@ components: default: false accountInfo: $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' - PlatformExternalAccountCreateRequest: - type: object - required: - - currency - - accountInfo - properties: - currency: - type: string - description: The ISO 4217 currency code - example: USD - platformAccountId: - type: string - description: Your platform's identifier for the account in your system. This can be used to reference the account by your own identifier. - example: ext_acc_123456 - ownershipType: - $ref: '#/components/schemas/OwnershipType' - accountInfo: - $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' - OwnershipVerificationState: + OwnershipVerificationMethod: type: string enum: - - PENDING - - PENDING_REVIEW - - VERIFIED - - FAILED + - WALLET_SIGNATURE + - LIVENESS description: | - Current status of this ownership verification. + The method used to verify ownership of a self-custody crypto wallet. - | Status | Description | + | Method | Description | |--------|-------------| - | `PENDING` | Awaiting the wallet signature or the user completing the verification flow | - | `PENDING_REVIEW` | Submitted and under review | - | `VERIFIED` | Ownership was verified. Terminal | - | `FAILED` | This verification attempt failed. Terminal; start a new verification to retry | - example: PENDING - WalletSignatureOwnershipVerification: - title: Wallet Signature Ownership Verification + | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | + | `LIVENESS` | Prove identity via a hosted biometric verification flow | + example: WALLET_SIGNATURE + OwnershipChallengeRequest: type: object - description: An ownership verification completed by signing a message with the wallet's key. + description: Starts (or restarts) an ownership verification challenge for a crypto wallet external account. + required: + - method + properties: + method: + $ref: '#/components/schemas/OwnershipVerificationMethod' + description: The verification method to use for this challenge. + WalletSignatureChallenge: + title: Wallet Signature Challenge + type: object + description: A challenge to prove ownership of the wallet by signing a message with the wallet's key. required: - - id - - externalAccountId - method - - status - messageToSign - expiresAt - - createdAt properties: - id: - type: string - description: Unique identifier for this ownership verification - example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 - externalAccountId: - type: string - description: The ID of the external account whose ownership is being verified - example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 method: type: string enum: - WALLET_SIGNATURE description: The verification method. Always `WALLET_SIGNATURE` for this shape. example: WALLET_SIGNATURE - status: - $ref: '#/components/schemas/OwnershipVerificationState' messageToSign: type: string - description: The exact message the wallet must sign, character-for-character. + description: The exact message the wallet must sign, character-for-character. Submit the resulting signature via the verify endpoint. example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' expiresAt: type: string format: date-time - description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. + description: When this challenge expires. Prompt the user promptly; after expiry, start a new challenge. example: '2025-08-15T15:32:00Z' - createdAt: - type: string - format: date-time - description: When this verification was created - example: '2025-08-15T15:02:00Z' - updatedAt: - type: string - format: date-time - description: When this verification was last updated - example: '2025-08-15T15:02:00Z' - LivenessOwnershipVerification: - title: Liveness Ownership Verification + LivenessChallenge: + title: Liveness Challenge type: object - description: An ownership verification completed by the user through a hosted biometric verification flow. + description: A challenge to prove ownership through a hosted biometric verification flow. Completes asynchronously — the outcome is delivered via `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the external account. required: - - id - - externalAccountId - method - - status - verificationLink - expiresAt - - createdAt properties: - id: - type: string - description: Unique identifier for this ownership verification - example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 - externalAccountId: - type: string - description: The ID of the external account whose ownership is being verified - example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 method: type: string enum: - LIVENESS description: The verification method. Always `LIVENESS` for this shape. example: LIVENESS - status: - $ref: '#/components/schemas/OwnershipVerificationState' verificationLink: type: string format: uri @@ -20684,90 +20621,28 @@ components: expiresAt: type: string format: date-time - description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. + description: When this challenge expires. Prompt the user promptly; after expiry, start a new challenge. example: '2025-08-15T15:32:00Z' - createdAt: - type: string - format: date-time - description: When this verification was created - example: '2025-08-15T15:02:00Z' - updatedAt: - type: string - format: date-time - description: When this verification was last updated - example: '2025-08-15T15:02:00Z' - OwnershipVerification: - description: An ownership verification for a self-custody crypto wallet external account. The shape is determined by the verification `method`. + OwnershipChallenge: + description: An ownership verification challenge for a crypto wallet external account. The shape is determined by the challenge `method`. oneOf: - - $ref: '#/components/schemas/WalletSignatureOwnershipVerification' - - $ref: '#/components/schemas/LivenessOwnershipVerification' + - $ref: '#/components/schemas/WalletSignatureChallenge' + - $ref: '#/components/schemas/LivenessChallenge' discriminator: propertyName: method mapping: - WALLET_SIGNATURE: '#/components/schemas/WalletSignatureOwnershipVerification' - LIVENESS: '#/components/schemas/LivenessOwnershipVerification' - OwnershipVerificationListResponse: - type: object - required: - - data - - hasMore - properties: - data: - type: array - description: List of ownership verifications matching the filter criteria - items: - $ref: '#/components/schemas/OwnershipVerification' - 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 results matching the criteria - OwnershipVerificationMethod: - type: string - enum: - - WALLET_SIGNATURE - - LIVENESS - description: | - The method used to verify ownership of a self-custody crypto wallet. - - | Method | Description | - |--------|-------------| - | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | - | `LIVENESS` | Prove identity via a hosted biometric verification flow | - example: WALLET_SIGNATURE - OwnershipVerificationRequest: + WALLET_SIGNATURE: '#/components/schemas/WalletSignatureChallenge' + LIVENESS: '#/components/schemas/LivenessChallenge' + OwnershipVerifyRequest: type: object - description: Creates an ownership verification for a self-custody crypto wallet external account. - required: - - externalAccountId - - method - properties: - externalAccountId: - type: string - description: The ID of the external account (self-custody crypto wallet) whose ownership is being verified. - example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: - $ref: '#/components/schemas/OwnershipVerificationMethod' - description: The verification method to use. - OwnershipVerificationConfirmRequest: - type: object - description: Completes a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the verification's `messageToSign`. + description: Completes a `WALLET_SIGNATURE` challenge by submitting the signature the wallet produced for the challenge's `messageToSign`. required: - signature - - signedAddress properties: signature: type: string description: The signature produced over the exact `messageToSign` — EIP-191 hex for EVM chains, base64 for Bitcoin, base58-encoded Ed25519 for Solana. example: '0x52d75f01c9e7b8b2ce2fbcbd21bfeeee7bcd1a2f01ce6b8ad9a67a45e83a8f5d1c' - signedAddress: - type: string - description: The wallet address that signed the message. - example: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' signatureScheme: type: string enum: @@ -20775,6 +20650,24 @@ components: - electrum default: bip137 description: Bitcoin message-signing format. Defaults to `bip137`; use `electrum` for Electrum/Sparrow wallets. Ignored for non-Bitcoin chains. + PlatformExternalAccountCreateRequest: + type: object + required: + - currency + - accountInfo + properties: + currency: + type: string + description: The ISO 4217 currency code + example: USD + platformAccountId: + type: string + description: Your platform's identifier for the account in your system. This can be used to reference the account by your own identifier. + example: ext_acc_123456 + ownershipType: + $ref: '#/components/schemas/OwnershipType' + accountInfo: + $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' BeneficialOwnerListResponse: type: object required: @@ -25325,9 +25218,6 @@ components: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION - - OWNERSHIP_VERIFICATION.PENDING_REVIEW - - OWNERSHIP_VERIFICATION.VERIFIED - - OWNERSHIP_VERIFICATION.FAILED - EXTERNAL_ACCOUNT.STATUS_UPDATED - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED @@ -25553,21 +25443,6 @@ components: - VERIFICATION.RESOLVE_ERRORS - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - OwnershipVerificationWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/OwnershipVerification' - type: - type: string - enum: - - OWNERSHIP_VERIFICATION.PENDING_REVIEW - - OWNERSHIP_VERIFICATION.VERIFIED - - OWNERSHIP_VERIFICATION.FAILED CardStateChangeWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' diff --git a/mintlify/style.css b/mintlify/style.css index 7d2b5167d..0131dfb69 100644 --- a/mintlify/style.css +++ b/mintlify/style.css @@ -1330,12 +1330,6 @@ ul.sidebar-group > li[data-title="External Accounts"] > button::before { background-image: url('/images/icons/bank.svg') !important; } -/* Ownership Verifications - shield */ -.sidebar-group > li[data-title="Ownership Verifications"] > button::before, -ul.sidebar-group > li[data-title="Ownership Verifications"] > button::before { - background-image: url('/images/icons/shield.svg') !important; -} - /* Same-Currency Transfers - horizontal expand arrows */ .sidebar-group > li[data-title="Same-Currency Transfers"] > button::before, ul.sidebar-group > li[data-title="Same-Currency Transfers"] > button::before { diff --git a/openapi.yaml b/openapi.yaml index da2b9b5b2..894642231 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -33,8 +33,6 @@ tags: description: Internal account management endpoints for creating and managing internal accounts - name: External Accounts description: External account management endpoints for creating and managing external bank accounts - - name: Ownership Verifications - description: Endpoints for verifying ownership of self-custody crypto wallet external accounts, via wallet signature or a hosted biometric verification flow. - name: Same-Currency Transfers description: Endpoints for transferring funds between internal and external accounts with the same currency - name: Cross-Currency Transfers @@ -2431,6 +2429,154 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /customers/external-accounts/{externalAccountId}/challenge: + post: + summary: Start an ownership verification challenge + description: | + Start (or restart) ownership verification for a `FIRST_PARTY` self-custody + crypto wallet external account in `PENDING_OWNERSHIP_VERIFICATION` or + `UNVERIFIED` status. The response carries the method-specific challenge + material: + + - `WALLET_SIGNATURE` — a `messageToSign`; have the wallet sign it exactly + and submit the result to the verify endpoint to complete verification + synchronously. + - `LIVENESS` — a hosted `verificationLink` (and possibly an embed + `token`); the user completes a biometric flow and verification completes + asynchronously. The outcome is delivered via + `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account. + + Calling this endpoint again abandons any in-flight challenge and issues a + new one with the requested method — use it to retry after a failed + attempt, to replace an expired challenge, or to switch methods. An + `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a + new challenge is issued. + + Completing ownership verification moves the account to `ACTIVE`. + operationId: createExternalAccountOwnershipChallenge + tags: + - External Accounts + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipChallengeRequest' + responses: + '201': + description: Challenge created; the method-specific material is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipChallenge' + '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: External account not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: Ownership verification is not applicable to this external account (not a self-custody crypto wallet, not `FIRST_PARTY`, or already verified). + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' + /customers/external-accounts/{externalAccountId}/verify: + post: + summary: Verify ownership with a wallet signature + description: | + Complete a `WALLET_SIGNATURE` challenge by submitting the signature the + wallet produced for the challenge's `messageToSign`. The message must be + signed exactly as returned, and the signature must be submitted before + the challenge's `expiresAt` — after expiry, start a new challenge. + + On success the account moves to `ACTIVE`; on an invalid signature it + moves to `UNVERIFIED` (start a new challenge to retry). `LIVENESS` + challenges complete asynchronously and never use this endpoint — their + outcome is delivered via `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by + polling the account. + operationId: verifyExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerifyRequest' + responses: + '200': + description: 'Signature valid; the updated external account is returned with `status: ACTIVE`.' + content: + application/json: + schema: + $ref: '#/components/schemas/ExternalAccount' + '400': + description: Invalid or expired signature. The account moves to `UNVERIFIED`; start a new challenge to retry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error400' + '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' + '409': + description: No `WALLET_SIGNATURE` challenge is outstanding for this external account. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + application/json: + schema: + $ref: '#/components/schemas/Error500' /platform/external-accounts: get: summary: List platform external accounts @@ -2635,45 +2781,55 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - /ownership-verifications: + /platform/external-accounts/{externalAccountId}/challenge: post: - summary: Create an ownership verification + summary: Start an ownership verification challenge description: | - Begin ownership verification for a `FIRST_PARTY` self-custody crypto - wallet external account (customer or platform owned). Choose a `method`: + Start (or restart) ownership verification for a `FIRST_PARTY` self-custody + crypto wallet external account in `PENDING_OWNERSHIP_VERIFICATION` or + `UNVERIFIED` status. The response carries the method-specific challenge + material: - - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the - wallet sign it and submit the result to - `POST /ownership-verifications/{verificationId}/confirm` to complete - verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` and may include a - `token` for embedding; the user completes a hosted biometric flow and - verification completes asynchronously. Status transitions are delivered - via `OWNERSHIP_VERIFICATION.*` webhooks or by polling - `GET /ownership-verifications/{verificationId}`. + - `WALLET_SIGNATURE` — a `messageToSign`; have the wallet sign it exactly + and submit the result to the verify endpoint to complete verification + synchronously. + - `LIVENESS` — a hosted `verificationLink` (and possibly an embed + `token`); the user completes a biometric flow and verification completes + asynchronously. The outcome is delivered via + `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account. - Ownership verification applies to accounts in - `PENDING_OWNERSHIP_VERIFICATION` status; completing it moves the account - to `ACTIVE`. For accounts where ownership verification is not applicable, - this returns `409`. - operationId: createOwnershipVerification + Calling this endpoint again abandons any in-flight challenge and issues a + new one with the requested method — use it to retry after a failed + attempt, to replace an expired challenge, or to switch methods. An + `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a + new challenge is issued. + + Completing ownership verification moves the account to `ACTIVE`. + operationId: createPlatformExternalAccountOwnershipChallenge tags: - - Ownership Verifications + - External Accounts security: - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerificationRequest' + $ref: '#/components/schemas/OwnershipChallengeRequest' responses: '201': - description: Ownership verification created; the method-specific material is returned. + description: Challenge created; the method-specific material is returned. content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerification' + $ref: '#/components/schemas/OwnershipChallenge' '400': description: Bad request - Invalid parameters content: @@ -2693,7 +2849,7 @@ paths: schema: $ref: '#/components/schemas/Error404' '409': - description: Ownership verification is not applicable to this external account. + description: Ownership verification is not applicable to this external account (not a self-custody crypto wallet, not `FIRST_PARTY`, or already verified). content: application/json: schema: @@ -2704,133 +2860,29 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - get: - summary: List ownership verifications - description: | - Retrieve a list of ownership verifications with optional filtering by external account ID and status. - operationId: listOwnershipVerifications - tags: - - Ownership Verifications - security: - - BasicAuth: [] - parameters: - - name: externalAccountId - in: query - description: Filter by external account ID - required: false - schema: - type: string - - name: status - in: query - description: Filter by verification status - required: false - schema: - $ref: '#/components/schemas/OwnershipVerificationState' - - 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/OwnershipVerificationListResponse' - '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' - /ownership-verifications/{verificationId}: - get: - summary: Get an ownership verification - description: Retrieve details of a specific ownership verification by ID. - operationId: getOwnershipVerification - tags: - - Ownership Verifications - security: - - BasicAuth: [] - parameters: - - name: verificationId - in: path - description: Ownership verification ID - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerification' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/Error401' - '404': - description: Ownership verification not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error404' - '500': - description: Internal service error - content: - application/json: - schema: - $ref: '#/components/schemas/Error500' - /ownership-verifications/{verificationId}/confirm: + /platform/external-accounts/{externalAccountId}/verify: post: - summary: Confirm an ownership verification + summary: Verify ownership with a wallet signature description: | - Complete a `WALLET_SIGNATURE` ownership verification by submitting the - signature the wallet produced for the `messageToSign` returned by - `POST /ownership-verifications`. The message must be signed exactly as - returned, and the signature must be submitted before the session's - `expiresAt`; after expiry, start a new verification. + Complete a `WALLET_SIGNATURE` challenge by submitting the signature the + wallet produced for the challenge's `messageToSign`. The message must be + signed exactly as returned, and the signature must be submitted before + the challenge's `expiresAt` — after expiry, start a new challenge. - This endpoint is only valid for `WALLET_SIGNATURE` verifications in - `PENDING` status. For other verifications, this returns `409`. `LIVENESS` - verifications complete asynchronously — their status is delivered via - `OWNERSHIP_VERIFICATION.*` webhooks or by polling - `GET /ownership-verifications/{verificationId}`. - operationId: confirmOwnershipVerification + On success the account moves to `ACTIVE`; on an invalid signature it + moves to `UNVERIFIED` (start a new challenge to retry). `LIVENESS` + challenges complete asynchronously and never use this endpoint — their + outcome is delivered via `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by + polling the account. + operationId: verifyPlatformExternalAccountOwnership tags: - - Ownership Verifications + - External Accounts security: - BasicAuth: [] parameters: - - name: verificationId + - name: externalAccountId in: path - description: Ownership verification ID + description: External account ID required: true schema: type: string @@ -2839,16 +2891,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerificationConfirmRequest' + $ref: '#/components/schemas/OwnershipVerifyRequest' responses: '200': - description: Signature submitted; the updated ownership verification is returned. + description: 'Signature valid; the updated external account is returned with `status: ACTIVE`.' content: application/json: schema: - $ref: '#/components/schemas/OwnershipVerification' + $ref: '#/components/schemas/ExternalAccount' '400': - description: Invalid or expired signature + description: Invalid or expired signature. The account moves to `UNVERIFIED`; start a new challenge to retry. content: application/json: schema: @@ -2860,13 +2912,13 @@ paths: schema: $ref: '#/components/schemas/Error401' '404': - description: Ownership verification not found + description: External account not found content: application/json: schema: $ref: '#/components/schemas/Error404' '409': - description: The verification is not a `WALLET_SIGNATURE` verification in `PENDING` status. + description: No `WALLET_SIGNATURE` challenge is outstanding for this external account. content: application/json: schema: @@ -10991,7 +11043,7 @@ webhooks: post: summary: External account status webhook description: | - Webhook that is called whenever the status of an external account changes, for any transition between statuses (`PENDING`, `ACTIVE`, `UNDER_REVIEW`, `INACTIVE`, `PENDING_OWNERSHIP_VERIFICATION`) — for example when an account under review becomes active, or when ownership verification completes. + Webhook that is called whenever the status of an external account changes, for any transition between statuses (`PENDING`, `ACTIVE`, `UNDER_REVIEW`, `INACTIVE`, `PENDING_OWNERSHIP_VERIFICATION`, `UNVERIFIED`) — for example when an account under review becomes active, when ownership verification completes (`PENDING_OWNERSHIP_VERIFICATION` → `ACTIVE`), or when a verification attempt fails (`PENDING_OWNERSHIP_VERIFICATION` → `UNVERIFIED`). This endpoint should be implemented by clients of the Grid API. ### Authentication @@ -11018,7 +11070,7 @@ webhooks: schema: $ref: '#/components/schemas/ExternalAccountStatusWebhook' examples: - statusUpdated: + verificationSucceeded: summary: A wallet account became active after ownership verification value: id: Webhook:019542f5-b3e7-1d02-0000-000000000042 @@ -11033,6 +11085,21 @@ webhooks: accountInfo: accountType: ETHEREUM_WALLET address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + verificationFailed: + summary: A liveness check failed; a new challenge is needed to retry + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000043 + type: EXTERNAL_ACCOUNT.STATUS_UPDATED + timestamp: '2025-08-15T14:45:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: UNVERIFIED + currency: USDC + ownershipType: FIRST_PARTY + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' responses: '200': description: | @@ -11134,92 +11201,6 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' - ownership-verification: - post: - summary: Ownership verification status change - description: | - Webhook that is called when the status of an ownership verification 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. - - ### Event types - - `OWNERSHIP_VERIFICATION.PENDING_REVIEW` — Fired when a submitted ownership verification enters review. The `data` payload contains the full ownership verification object. - - `OWNERSHIP_VERIFICATION.VERIFIED` — Fired when ownership of the external account has been verified. The `data` payload contains the full ownership verification object. - - `OWNERSHIP_VERIFICATION.FAILED` — Fired when an ownership verification attempt fails; start a new verification to retry. The `data` payload contains the full ownership verification object. - operationId: ownershipVerificationWebhook - tags: - - Webhooks - security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OwnershipVerificationWebhook' - examples: - verified: - summary: Ownership of a self-custody wallet has been verified - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000040 - type: OWNERSHIP_VERIFICATION.VERIFIED - timestamp: '2025-08-15T15:10:00Z' - data: - id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 - externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: WALLET_SIGNATURE - status: VERIFIED - messageToSign: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' - expiresAt: '2025-08-15T15:32:00Z' - createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T15:10:00Z' - failed: - summary: An ownership verification attempt failed - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000041 - type: OWNERSHIP_VERIFICATION.FAILED - timestamp: '2025-08-15T15:10:00Z' - data: - id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 - externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: LIVENESS - status: FAILED - verificationLink: https://verify.example.com/session/019542f5-b3e7-1d02 - token: eyJhbGciOiJIUzI1NiJ9.example - expiresAt: '2025-08-15T15:32:00Z' - createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T15:10:00Z' - 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' card-state-change: post: summary: Card state change @@ -17249,6 +17230,7 @@ components: - PENDING - ACTIVE - PENDING_OWNERSHIP_VERIFICATION + - UNVERIFIED - UNDER_REVIEW - INACTIVE description: | @@ -17257,10 +17239,16 @@ components: `PENDING_OWNERSHIP_VERIFICATION` applies to crypto wallet accounts whose ownership must be verified before the account can be used without restriction — for example, under the EU Travel Rule or similar requirements - in other regions. While in this status, the account can be used for - transfers below regulatory thresholds; completing ownership verification - (see the Ownership Verifications API) moves the account to `ACTIVE` and - removes the restriction. + in other regions. It covers both "verification not yet started" and + "verification in progress" (e.g. a liveness check underway). While in this + status, the account can be used for transfers below regulatory thresholds. + + `UNVERIFIED` means the most recent ownership verification attempt failed. + The account keeps the same below-threshold capabilities as + `PENDING_OWNERSHIP_VERIFICATION`; start a new challenge to retry, which + returns the account to `PENDING_OWNERSHIP_VERIFICATION`. + + Completing ownership verification moves the account to `ACTIVE`. OwnershipType: type: string enum: @@ -20560,118 +20548,67 @@ components: default: false accountInfo: $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' - PlatformExternalAccountCreateRequest: - type: object - required: - - currency - - accountInfo - properties: - currency: - type: string - description: The ISO 4217 currency code - example: USD - platformAccountId: - type: string - description: Your platform's identifier for the account in your system. This can be used to reference the account by your own identifier. - example: ext_acc_123456 - ownershipType: - $ref: '#/components/schemas/OwnershipType' - accountInfo: - $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' - OwnershipVerificationState: + OwnershipVerificationMethod: type: string enum: - - PENDING - - PENDING_REVIEW - - VERIFIED - - FAILED + - WALLET_SIGNATURE + - LIVENESS description: | - Current status of this ownership verification. + The method used to verify ownership of a self-custody crypto wallet. - | Status | Description | + | Method | Description | |--------|-------------| - | `PENDING` | Awaiting the wallet signature or the user completing the verification flow | - | `PENDING_REVIEW` | Submitted and under review | - | `VERIFIED` | Ownership was verified. Terminal | - | `FAILED` | This verification attempt failed. Terminal; start a new verification to retry | - example: PENDING - WalletSignatureOwnershipVerification: - title: Wallet Signature Ownership Verification + | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | + | `LIVENESS` | Prove identity via a hosted biometric verification flow | + example: WALLET_SIGNATURE + OwnershipChallengeRequest: type: object - description: An ownership verification completed by signing a message with the wallet's key. + description: Starts (or restarts) an ownership verification challenge for a crypto wallet external account. + required: + - method + properties: + method: + $ref: '#/components/schemas/OwnershipVerificationMethod' + description: The verification method to use for this challenge. + WalletSignatureChallenge: + title: Wallet Signature Challenge + type: object + description: A challenge to prove ownership of the wallet by signing a message with the wallet's key. required: - - id - - externalAccountId - method - - status - messageToSign - expiresAt - - createdAt properties: - id: - type: string - description: Unique identifier for this ownership verification - example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 - externalAccountId: - type: string - description: The ID of the external account whose ownership is being verified - example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 method: type: string enum: - WALLET_SIGNATURE description: The verification method. Always `WALLET_SIGNATURE` for this shape. example: WALLET_SIGNATURE - status: - $ref: '#/components/schemas/OwnershipVerificationState' messageToSign: type: string - description: The exact message the wallet must sign, character-for-character. + description: The exact message the wallet must sign, character-for-character. Submit the resulting signature via the verify endpoint. example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' expiresAt: type: string format: date-time - description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. + description: When this challenge expires. Prompt the user promptly; after expiry, start a new challenge. example: '2025-08-15T15:32:00Z' - createdAt: - type: string - format: date-time - description: When this verification was created - example: '2025-08-15T15:02:00Z' - updatedAt: - type: string - format: date-time - description: When this verification was last updated - example: '2025-08-15T15:02:00Z' - LivenessOwnershipVerification: - title: Liveness Ownership Verification + LivenessChallenge: + title: Liveness Challenge type: object - description: An ownership verification completed by the user through a hosted biometric verification flow. + description: A challenge to prove ownership through a hosted biometric verification flow. Completes asynchronously — the outcome is delivered via `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the external account. required: - - id - - externalAccountId - method - - status - verificationLink - expiresAt - - createdAt properties: - id: - type: string - description: Unique identifier for this ownership verification - example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 - externalAccountId: - type: string - description: The ID of the external account whose ownership is being verified - example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 method: type: string enum: - LIVENESS description: The verification method. Always `LIVENESS` for this shape. example: LIVENESS - status: - $ref: '#/components/schemas/OwnershipVerificationState' verificationLink: type: string format: uri @@ -20684,90 +20621,28 @@ components: expiresAt: type: string format: date-time - description: When this verification session expires. Prompt the user promptly; after expiry, a new verification must be started. + description: When this challenge expires. Prompt the user promptly; after expiry, start a new challenge. example: '2025-08-15T15:32:00Z' - createdAt: - type: string - format: date-time - description: When this verification was created - example: '2025-08-15T15:02:00Z' - updatedAt: - type: string - format: date-time - description: When this verification was last updated - example: '2025-08-15T15:02:00Z' - OwnershipVerification: - description: An ownership verification for a self-custody crypto wallet external account. The shape is determined by the verification `method`. + OwnershipChallenge: + description: An ownership verification challenge for a crypto wallet external account. The shape is determined by the challenge `method`. oneOf: - - $ref: '#/components/schemas/WalletSignatureOwnershipVerification' - - $ref: '#/components/schemas/LivenessOwnershipVerification' + - $ref: '#/components/schemas/WalletSignatureChallenge' + - $ref: '#/components/schemas/LivenessChallenge' discriminator: propertyName: method mapping: - WALLET_SIGNATURE: '#/components/schemas/WalletSignatureOwnershipVerification' - LIVENESS: '#/components/schemas/LivenessOwnershipVerification' - OwnershipVerificationListResponse: - type: object - required: - - data - - hasMore - properties: - data: - type: array - description: List of ownership verifications matching the filter criteria - items: - $ref: '#/components/schemas/OwnershipVerification' - 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 results matching the criteria - OwnershipVerificationMethod: - type: string - enum: - - WALLET_SIGNATURE - - LIVENESS - description: | - The method used to verify ownership of a self-custody crypto wallet. - - | Method | Description | - |--------|-------------| - | `WALLET_SIGNATURE` | Prove control of the wallet by signing a message with the wallet's key | - | `LIVENESS` | Prove identity via a hosted biometric verification flow | - example: WALLET_SIGNATURE - OwnershipVerificationRequest: + WALLET_SIGNATURE: '#/components/schemas/WalletSignatureChallenge' + LIVENESS: '#/components/schemas/LivenessChallenge' + OwnershipVerifyRequest: type: object - description: Creates an ownership verification for a self-custody crypto wallet external account. - required: - - externalAccountId - - method - properties: - externalAccountId: - type: string - description: The ID of the external account (self-custody crypto wallet) whose ownership is being verified. - example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: - $ref: '#/components/schemas/OwnershipVerificationMethod' - description: The verification method to use. - OwnershipVerificationConfirmRequest: - type: object - description: Completes a `WALLET_SIGNATURE` ownership verification by submitting the signature the wallet produced for the verification's `messageToSign`. + description: Completes a `WALLET_SIGNATURE` challenge by submitting the signature the wallet produced for the challenge's `messageToSign`. required: - signature - - signedAddress properties: signature: type: string description: The signature produced over the exact `messageToSign` — EIP-191 hex for EVM chains, base64 for Bitcoin, base58-encoded Ed25519 for Solana. example: '0x52d75f01c9e7b8b2ce2fbcbd21bfeeee7bcd1a2f01ce6b8ad9a67a45e83a8f5d1c' - signedAddress: - type: string - description: The wallet address that signed the message. - example: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' signatureScheme: type: string enum: @@ -20775,6 +20650,24 @@ components: - electrum default: bip137 description: Bitcoin message-signing format. Defaults to `bip137`; use `electrum` for Electrum/Sparrow wallets. Ignored for non-Bitcoin chains. + PlatformExternalAccountCreateRequest: + type: object + required: + - currency + - accountInfo + properties: + currency: + type: string + description: The ISO 4217 currency code + example: USD + platformAccountId: + type: string + description: Your platform's identifier for the account in your system. This can be used to reference the account by your own identifier. + example: ext_acc_123456 + ownershipType: + $ref: '#/components/schemas/OwnershipType' + accountInfo: + $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' BeneficialOwnerListResponse: type: object required: @@ -25325,9 +25218,6 @@ components: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION - - OWNERSHIP_VERIFICATION.PENDING_REVIEW - - OWNERSHIP_VERIFICATION.VERIFIED - - OWNERSHIP_VERIFICATION.FAILED - EXTERNAL_ACCOUNT.STATUS_UPDATED - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED @@ -25553,21 +25443,6 @@ components: - VERIFICATION.RESOLVE_ERRORS - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - OwnershipVerificationWebhook: - allOf: - - $ref: '#/components/schemas/BaseWebhook' - - type: object - required: - - data - properties: - data: - $ref: '#/components/schemas/OwnershipVerification' - type: - type: string - enum: - - OWNERSHIP_VERIFICATION.PENDING_REVIEW - - OWNERSHIP_VERIFICATION.VERIFIED - - OWNERSHIP_VERIFICATION.FAILED CardStateChangeWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' diff --git a/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml b/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml index c16c9513e..2b98bc325 100644 --- a/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml +++ b/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml @@ -3,6 +3,7 @@ enum: - PENDING - ACTIVE - PENDING_OWNERSHIP_VERIFICATION + - UNVERIFIED - UNDER_REVIEW - INACTIVE description: | @@ -11,7 +12,13 @@ description: | `PENDING_OWNERSHIP_VERIFICATION` applies to crypto wallet accounts whose ownership must be verified before the account can be used without restriction — for example, under the EU Travel Rule or similar requirements - in other regions. While in this status, the account can be used for - transfers below regulatory thresholds; completing ownership verification - (see the Ownership Verifications API) moves the account to `ACTIVE` and - removes the restriction. + in other regions. It covers both "verification not yet started" and + "verification in progress" (e.g. a liveness check underway). While in this + status, the account can be used for transfers below regulatory thresholds. + + `UNVERIFIED` means the most recent ownership verification attempt failed. + The account keeps the same below-threshold capabilities as + `PENDING_OWNERSHIP_VERIFICATION`; start a new challenge to retry, which + returns the account to `PENDING_OWNERSHIP_VERIFICATION`. + + Completing ownership verification moves the account to `ACTIVE`. diff --git a/openapi/components/schemas/external_accounts/LivenessChallenge.yaml b/openapi/components/schemas/external_accounts/LivenessChallenge.yaml new file mode 100644 index 000000000..359a118fb --- /dev/null +++ b/openapi/components/schemas/external_accounts/LivenessChallenge.yaml @@ -0,0 +1,39 @@ +title: Liveness Challenge +type: object +description: >- + A challenge to prove ownership through a hosted biometric verification + flow. Completes asynchronously — the outcome is delivered via + `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the external + account. +required: + - method + - verificationLink + - expiresAt +properties: + method: + type: string + enum: + - LIVENESS + description: The verification method. Always `LIVENESS` for this shape. + example: LIVENESS + verificationLink: + type: string + format: uri + description: Hosted verification URL to present to the user. + example: https://verify.example.com/session/019542f5-b3e7-1d02 + token: + type: string + description: >- + Provider-specific token that can be used in place of `verificationLink` — + 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 verification outcome. + example: eyJhbGciOiJIUzI1NiJ9.example + expiresAt: + type: string + format: date-time + description: >- + When this challenge expires. Prompt the user promptly; after expiry, + start a new challenge. + example: '2025-08-15T15:32:00Z' diff --git a/openapi/components/schemas/external_accounts/OwnershipChallenge.yaml b/openapi/components/schemas/external_accounts/OwnershipChallenge.yaml new file mode 100644 index 000000000..534b7b6df --- /dev/null +++ b/openapi/components/schemas/external_accounts/OwnershipChallenge.yaml @@ -0,0 +1,11 @@ +description: >- + An ownership verification challenge for a crypto wallet external account. + The shape is determined by the challenge `method`. +oneOf: + - $ref: ./WalletSignatureChallenge.yaml + - $ref: ./LivenessChallenge.yaml +discriminator: + propertyName: method + mapping: + WALLET_SIGNATURE: ./WalletSignatureChallenge.yaml + LIVENESS: ./LivenessChallenge.yaml diff --git a/openapi/components/schemas/external_accounts/OwnershipChallengeRequest.yaml b/openapi/components/schemas/external_accounts/OwnershipChallengeRequest.yaml new file mode 100644 index 000000000..60b349f2c --- /dev/null +++ b/openapi/components/schemas/external_accounts/OwnershipChallengeRequest.yaml @@ -0,0 +1,10 @@ +type: object +description: >- + Starts (or restarts) an ownership verification challenge for a crypto + wallet external account. +required: + - method +properties: + method: + $ref: ./OwnershipVerificationMethod.yaml + description: The verification method to use for this challenge. diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerificationMethod.yaml b/openapi/components/schemas/external_accounts/OwnershipVerificationMethod.yaml similarity index 100% rename from openapi/components/schemas/ownership_verifications/OwnershipVerificationMethod.yaml rename to openapi/components/schemas/external_accounts/OwnershipVerificationMethod.yaml diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml b/openapi/components/schemas/external_accounts/OwnershipVerifyRequest.yaml similarity index 65% rename from openapi/components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml rename to openapi/components/schemas/external_accounts/OwnershipVerifyRequest.yaml index 3f275e2de..44d48abeb 100644 --- a/openapi/components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml +++ b/openapi/components/schemas/external_accounts/OwnershipVerifyRequest.yaml @@ -1,10 +1,9 @@ type: object description: >- - Completes a `WALLET_SIGNATURE` ownership verification by submitting the - signature the wallet produced for the verification's `messageToSign`. + Completes a `WALLET_SIGNATURE` challenge by submitting the signature the + wallet produced for the challenge's `messageToSign`. required: - signature - - signedAddress properties: signature: type: string @@ -12,10 +11,6 @@ properties: The signature produced over the exact `messageToSign` — EIP-191 hex for EVM chains, base64 for Bitcoin, base58-encoded Ed25519 for Solana. example: '0x52d75f01c9e7b8b2ce2fbcbd21bfeeee7bcd1a2f01ce6b8ad9a67a45e83a8f5d1c' - signedAddress: - type: string - description: The wallet address that signed the message. - example: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' signatureScheme: type: string enum: diff --git a/openapi/components/schemas/external_accounts/WalletSignatureChallenge.yaml b/openapi/components/schemas/external_accounts/WalletSignatureChallenge.yaml new file mode 100644 index 000000000..d6fd7dbe6 --- /dev/null +++ b/openapi/components/schemas/external_accounts/WalletSignatureChallenge.yaml @@ -0,0 +1,29 @@ +title: Wallet Signature Challenge +type: object +description: >- + A challenge to prove ownership of the wallet by signing a message with the + wallet's key. +required: + - method + - messageToSign + - expiresAt +properties: + method: + type: string + enum: + - WALLET_SIGNATURE + description: The verification method. Always `WALLET_SIGNATURE` for this shape. + example: WALLET_SIGNATURE + messageToSign: + type: string + description: >- + The exact message the wallet must sign, character-for-character. Submit + the resulting signature via the verify endpoint. + example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' + expiresAt: + type: string + format: date-time + description: >- + When this challenge expires. Prompt the user promptly; after expiry, + start a new challenge. + example: '2025-08-15T15:32:00Z' diff --git a/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml b/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml deleted file mode 100644 index b565fc23c..000000000 --- a/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml +++ /dev/null @@ -1,61 +0,0 @@ -title: Liveness Ownership Verification -type: object -description: >- - An ownership verification completed by the user through a hosted biometric - verification flow. -required: - - id - - externalAccountId - - method - - status - - verificationLink - - expiresAt - - createdAt -properties: - id: - type: string - description: Unique identifier for this ownership verification - example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 - externalAccountId: - type: string - description: The ID of the external account whose ownership is being verified - example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: - type: string - enum: - - LIVENESS - description: The verification method. Always `LIVENESS` for this shape. - example: LIVENESS - status: - $ref: ./OwnershipVerificationState.yaml - verificationLink: - type: string - format: uri - description: Hosted verification URL to present to the user. - example: https://verify.example.com/session/019542f5-b3e7-1d02 - token: - type: string - description: >- - Provider-specific token that can be used in place of `verificationLink` — - 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 verification outcome. - example: eyJhbGciOiJIUzI1NiJ9.example - expiresAt: - type: string - format: date-time - description: >- - When this verification session expires. Prompt the user promptly; after - expiry, a new verification must be started. - example: '2025-08-15T15:32:00Z' - createdAt: - type: string - format: date-time - description: When this verification was created - example: '2025-08-15T15:02:00Z' - updatedAt: - type: string - format: date-time - description: When this verification was last updated - example: '2025-08-15T15:02:00Z' diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerification.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerification.yaml deleted file mode 100644 index 827239d18..000000000 --- a/openapi/components/schemas/ownership_verifications/OwnershipVerification.yaml +++ /dev/null @@ -1,11 +0,0 @@ -description: >- - An ownership verification for a self-custody crypto wallet external account. - The shape is determined by the verification `method`. -oneOf: - - $ref: ./WalletSignatureOwnershipVerification.yaml - - $ref: ./LivenessOwnershipVerification.yaml -discriminator: - propertyName: method - mapping: - WALLET_SIGNATURE: ./WalletSignatureOwnershipVerification.yaml - LIVENESS: ./LivenessOwnershipVerification.yaml diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerificationListResponse.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationListResponse.yaml deleted file mode 100644 index 87bfc4b93..000000000 --- a/openapi/components/schemas/ownership_verifications/OwnershipVerificationListResponse.yaml +++ /dev/null @@ -1,21 +0,0 @@ -type: object -required: - - data - - hasMore -properties: - data: - type: array - description: List of ownership verifications matching the filter criteria - items: - $ref: ./OwnershipVerification.yaml - 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 results matching the criteria diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml deleted file mode 100644 index d1feafe02..000000000 --- a/openapi/components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml +++ /dev/null @@ -1,17 +0,0 @@ -type: object -description: >- - Creates an ownership verification for a self-custody crypto wallet external - account. -required: - - externalAccountId - - method -properties: - externalAccountId: - type: string - description: >- - The ID of the external account (self-custody crypto wallet) whose - ownership is being verified. - example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: - $ref: ./OwnershipVerificationMethod.yaml - description: The verification method to use. diff --git a/openapi/components/schemas/ownership_verifications/OwnershipVerificationState.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationState.yaml deleted file mode 100644 index 3bbfb25fa..000000000 --- a/openapi/components/schemas/ownership_verifications/OwnershipVerificationState.yaml +++ /dev/null @@ -1,16 +0,0 @@ -type: string -enum: - - PENDING - - PENDING_REVIEW - - VERIFIED - - FAILED -description: | - Current status of this ownership verification. - - | Status | Description | - |--------|-------------| - | `PENDING` | Awaiting the wallet signature or the user completing the verification flow | - | `PENDING_REVIEW` | Submitted and under review | - | `VERIFIED` | Ownership was verified. Terminal | - | `FAILED` | This verification attempt failed. Terminal; start a new verification to retry | -example: PENDING diff --git a/openapi/components/schemas/ownership_verifications/WalletSignatureOwnershipVerification.yaml b/openapi/components/schemas/ownership_verifications/WalletSignatureOwnershipVerification.yaml deleted file mode 100644 index f5c9219a3..000000000 --- a/openapi/components/schemas/ownership_verifications/WalletSignatureOwnershipVerification.yaml +++ /dev/null @@ -1,51 +0,0 @@ -title: Wallet Signature Ownership Verification -type: object -description: >- - An ownership verification completed by signing a message with the wallet's - key. -required: - - id - - externalAccountId - - method - - status - - messageToSign - - expiresAt - - createdAt -properties: - id: - type: string - description: Unique identifier for this ownership verification - example: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 - externalAccountId: - type: string - description: The ID of the external account whose ownership is being verified - example: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: - type: string - enum: - - WALLET_SIGNATURE - description: The verification method. Always `WALLET_SIGNATURE` for this shape. - example: WALLET_SIGNATURE - status: - $ref: ./OwnershipVerificationState.yaml - messageToSign: - type: string - description: The exact message the wallet must sign, character-for-character. - example: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' - expiresAt: - type: string - format: date-time - description: >- - When this verification session expires. Prompt the user promptly; after - expiry, a new verification must be started. - example: '2025-08-15T15:32:00Z' - createdAt: - type: string - format: date-time - description: When this verification was created - example: '2025-08-15T15:02:00Z' - updatedAt: - type: string - format: date-time - description: When this verification was last updated - example: '2025-08-15T15:02:00Z' diff --git a/openapi/components/schemas/webhooks/OwnershipVerificationWebhook.yaml b/openapi/components/schemas/webhooks/OwnershipVerificationWebhook.yaml deleted file mode 100644 index 3b714288a..000000000 --- a/openapi/components/schemas/webhooks/OwnershipVerificationWebhook.yaml +++ /dev/null @@ -1,14 +0,0 @@ -allOf: - - $ref: ./BaseWebhook.yaml - - type: object - required: - - data - properties: - data: - $ref: ../ownership_verifications/OwnershipVerification.yaml - type: - type: string - enum: - - OWNERSHIP_VERIFICATION.PENDING_REVIEW - - OWNERSHIP_VERIFICATION.VERIFIED - - OWNERSHIP_VERIFICATION.FAILED diff --git a/openapi/components/schemas/webhooks/WebhookType.yaml b/openapi/components/schemas/webhooks/WebhookType.yaml index 3844b5725..d27f1b6e2 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -26,9 +26,6 @@ enum: - VERIFICATION.IN_PROGRESS - VERIFICATION.PENDING_MANUAL_REVIEW - VERIFICATION.READY_FOR_VERIFICATION - - OWNERSHIP_VERIFICATION.PENDING_REVIEW - - OWNERSHIP_VERIFICATION.VERIFIED - - OWNERSHIP_VERIFICATION.FAILED - EXTERNAL_ACCOUNT.STATUS_UPDATED - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 8156b8482..c3dc1e690 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -40,10 +40,6 @@ tags: description: Internal account management endpoints for creating and managing internal accounts - name: External Accounts description: External account management endpoints for creating and managing external bank accounts - - name: Ownership Verifications - description: >- - Endpoints for verifying ownership of self-custody crypto wallet external - accounts, via wallet signature or a hosted biometric verification flow. - name: Same-Currency Transfers description: Endpoints for transferring funds between internal and external accounts with the same currency - name: Cross-Currency Transfers @@ -193,16 +189,18 @@ paths: $ref: paths/customers/customers_external_accounts.yaml /customers/external-accounts/{externalAccountId}: $ref: paths/customers/customers_external_accounts_{externalAccountId}.yaml + /customers/external-accounts/{externalAccountId}/challenge: + $ref: paths/customers/customers_external_accounts_{externalAccountId}_challenge.yaml + /customers/external-accounts/{externalAccountId}/verify: + $ref: paths/customers/customers_external_accounts_{externalAccountId}_verify.yaml /platform/external-accounts: $ref: paths/platform/platform_external_accounts.yaml /platform/external-accounts/{externalAccountId}: $ref: paths/platform/platform_external_accounts_{externalAccountId}.yaml - /ownership-verifications: - $ref: paths/ownership_verifications/ownership-verifications.yaml - /ownership-verifications/{verificationId}: - $ref: paths/ownership_verifications/ownership-verifications_{verificationId}.yaml - /ownership-verifications/{verificationId}/confirm: - $ref: paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml + /platform/external-accounts/{externalAccountId}/challenge: + $ref: paths/platform/platform_external_accounts_{externalAccountId}_challenge.yaml + /platform/external-accounts/{externalAccountId}/verify: + $ref: paths/platform/platform_external_accounts_{externalAccountId}_verify.yaml /beneficial-owners: $ref: paths/beneficial-owners/beneficial_owners.yaml /beneficial-owners/{beneficialOwnerId}: @@ -403,8 +401,6 @@ webhooks: $ref: webhooks/external-account-status.yaml verification-update: $ref: webhooks/verification-update.yaml - ownership-verification: - $ref: webhooks/ownership-verification.yaml card-state-change: $ref: webhooks/card-state-change.yaml card-funding-source-change: diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_challenge.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_challenge.yaml new file mode 100644 index 000000000..abc37e643 --- /dev/null +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_challenge.yaml @@ -0,0 +1,81 @@ +post: + summary: Start an ownership verification challenge + description: | + Start (or restart) ownership verification for a `FIRST_PARTY` self-custody + crypto wallet external account in `PENDING_OWNERSHIP_VERIFICATION` or + `UNVERIFIED` status. The response carries the method-specific challenge + material: + + - `WALLET_SIGNATURE` — a `messageToSign`; have the wallet sign it exactly + and submit the result to the verify endpoint to complete verification + synchronously. + - `LIVENESS` — a hosted `verificationLink` (and possibly an embed + `token`); the user completes a biometric flow and verification completes + asynchronously. The outcome is delivered via + `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account. + + Calling this endpoint again abandons any in-flight challenge and issues a + new one with the requested method — use it to retry after a failed + attempt, to replace an expired challenge, or to switch methods. An + `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a + new challenge is issued. + + Completing ownership verification moves the account to `ACTIVE`. + operationId: createExternalAccountOwnershipChallenge + tags: + - External Accounts + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipChallengeRequest.yaml + responses: + '201': + description: Challenge created; the method-specific material is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipChallenge.yaml + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: External account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + Ownership verification is not applicable to this external account + (not a self-custody crypto wallet, not `FIRST_PARTY`, or already + verified). + 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}_verify.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify.yaml new file mode 100644 index 000000000..445e2832f --- /dev/null +++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_verify.yaml @@ -0,0 +1,74 @@ +post: + summary: Verify ownership with a wallet signature + description: | + Complete a `WALLET_SIGNATURE` challenge by submitting the signature the + wallet produced for the challenge's `messageToSign`. The message must be + signed exactly as returned, and the signature must be submitted before + the challenge's `expiresAt` — after expiry, start a new challenge. + + On success the account moves to `ACTIVE`; on an invalid signature it + moves to `UNVERIFIED` (start a new challenge to retry). `LIVENESS` + challenges complete asynchronously and never use this endpoint — their + outcome is delivered via `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by + polling the account. + operationId: verifyExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipVerifyRequest.yaml + responses: + '200': + description: >- + Signature valid; the updated external account is returned with + `status: ACTIVE`. + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/ExternalAccount.yaml + '400': + description: >- + Invalid or expired signature. The account moves to `UNVERIFIED`; + start a new challenge to retry. + 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: External account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + No `WALLET_SIGNATURE` challenge is outstanding for this external + account. + 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/ownership_verifications/ownership-verifications.yaml b/openapi/paths/ownership_verifications/ownership-verifications.yaml deleted file mode 100644 index 3625a972d..000000000 --- a/openapi/paths/ownership_verifications/ownership-verifications.yaml +++ /dev/null @@ -1,133 +0,0 @@ -post: - summary: Create an ownership verification - description: | - Begin ownership verification for a `FIRST_PARTY` self-custody crypto - wallet external account (customer or platform owned). Choose a `method`: - - - `WALLET_SIGNATURE` — the response includes a `messageToSign`; have the - wallet sign it and submit the result to - `POST /ownership-verifications/{verificationId}/confirm` to complete - verification synchronously. - - `LIVENESS` — the response includes a `verificationLink` and may include a - `token` for embedding; the user completes a hosted biometric flow and - verification completes asynchronously. Status transitions are delivered - via `OWNERSHIP_VERIFICATION.*` webhooks or by polling - `GET /ownership-verifications/{verificationId}`. - - Ownership verification applies to accounts in - `PENDING_OWNERSHIP_VERIFICATION` status; completing it moves the account - to `ACTIVE`. For accounts where ownership verification is not applicable, - this returns `409`. - operationId: createOwnershipVerification - tags: - - Ownership Verifications - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/ownership_verifications/OwnershipVerificationRequest.yaml - responses: - '201': - description: >- - Ownership verification created; the method-specific material is - returned. - content: - application/json: - schema: - $ref: ../../components/schemas/ownership_verifications/OwnershipVerification.yaml - '400': - description: Bad request - Invalid parameters - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error400.yaml - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error401.yaml - '404': - description: External account not found - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error404.yaml - '409': - description: Ownership verification is not applicable to this external account. - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error409.yaml - '500': - description: Internal service error - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error500.yaml -get: - summary: List ownership verifications - description: > - Retrieve a list of ownership verifications with optional filtering by - external account ID and status. - operationId: listOwnershipVerifications - tags: - - Ownership Verifications - security: - - BasicAuth: [] - parameters: - - name: externalAccountId - in: query - description: Filter by external account ID - required: false - schema: - type: string - - name: status - in: query - description: Filter by verification status - required: false - schema: - $ref: ../../components/schemas/ownership_verifications/OwnershipVerificationState.yaml - - 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/ownership_verifications/OwnershipVerificationListResponse.yaml - '400': - description: Bad request - Invalid parameters - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error400.yaml - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error401.yaml - '500': - description: Internal service error - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}.yaml b/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}.yaml deleted file mode 100644 index 3e608aea9..000000000 --- a/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}.yaml +++ /dev/null @@ -1,40 +0,0 @@ -get: - summary: Get an ownership verification - description: Retrieve details of a specific ownership verification by ID. - operationId: getOwnershipVerification - tags: - - Ownership Verifications - security: - - BasicAuth: [] - parameters: - - name: verificationId - in: path - description: Ownership verification ID - required: true - schema: - type: string - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: ../../components/schemas/ownership_verifications/OwnershipVerification.yaml - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error401.yaml - '404': - description: Ownership verification not found - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error404.yaml - '500': - description: Internal service error - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error500.yaml diff --git a/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml b/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml deleted file mode 100644 index bb686e60b..000000000 --- a/openapi/paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml +++ /dev/null @@ -1,72 +0,0 @@ -post: - summary: Confirm an ownership verification - description: | - Complete a `WALLET_SIGNATURE` ownership verification by submitting the - signature the wallet produced for the `messageToSign` returned by - `POST /ownership-verifications`. The message must be signed exactly as - returned, and the signature must be submitted before the session's - `expiresAt`; after expiry, start a new verification. - - This endpoint is only valid for `WALLET_SIGNATURE` verifications in - `PENDING` status. For other verifications, this returns `409`. `LIVENESS` - verifications complete asynchronously — their status is delivered via - `OWNERSHIP_VERIFICATION.*` webhooks or by polling - `GET /ownership-verifications/{verificationId}`. - operationId: confirmOwnershipVerification - tags: - - Ownership Verifications - security: - - BasicAuth: [] - parameters: - - name: verificationId - in: path - description: Ownership verification ID - required: true - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: ../../components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml - responses: - '200': - description: >- - Signature submitted; the updated ownership verification is returned. - content: - application/json: - schema: - $ref: ../../components/schemas/ownership_verifications/OwnershipVerification.yaml - '400': - description: Invalid or expired signature - 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: Ownership verification not found - content: - application/json: - schema: - $ref: ../../components/schemas/errors/Error404.yaml - '409': - description: >- - The verification is not a `WALLET_SIGNATURE` verification in `PENDING` - status. - 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/platform/platform_external_accounts_{externalAccountId}_challenge.yaml b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_challenge.yaml new file mode 100644 index 000000000..532decc60 --- /dev/null +++ b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_challenge.yaml @@ -0,0 +1,81 @@ +post: + summary: Start an ownership verification challenge + description: | + Start (or restart) ownership verification for a `FIRST_PARTY` self-custody + crypto wallet external account in `PENDING_OWNERSHIP_VERIFICATION` or + `UNVERIFIED` status. The response carries the method-specific challenge + material: + + - `WALLET_SIGNATURE` — a `messageToSign`; have the wallet sign it exactly + and submit the result to the verify endpoint to complete verification + synchronously. + - `LIVENESS` — a hosted `verificationLink` (and possibly an embed + `token`); the user completes a biometric flow and verification completes + asynchronously. The outcome is delivered via + `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account. + + Calling this endpoint again abandons any in-flight challenge and issues a + new one with the requested method — use it to retry after a failed + attempt, to replace an expired challenge, or to switch methods. An + `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a + new challenge is issued. + + Completing ownership verification moves the account to `ACTIVE`. + operationId: createPlatformExternalAccountOwnershipChallenge + tags: + - External Accounts + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipChallengeRequest.yaml + responses: + '201': + description: Challenge created; the method-specific material is returned. + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipChallenge.yaml + '400': + description: Bad request - Invalid parameters + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error400.yaml + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error401.yaml + '404': + description: External account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + Ownership verification is not applicable to this external account + (not a self-custody crypto wallet, not `FIRST_PARTY`, or already + verified). + 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/platform/platform_external_accounts_{externalAccountId}_verify.yaml b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify.yaml new file mode 100644 index 000000000..6d151d79e --- /dev/null +++ b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_verify.yaml @@ -0,0 +1,74 @@ +post: + summary: Verify ownership with a wallet signature + description: | + Complete a `WALLET_SIGNATURE` challenge by submitting the signature the + wallet produced for the challenge's `messageToSign`. The message must be + signed exactly as returned, and the signature must be submitted before + the challenge's `expiresAt` — after expiry, start a new challenge. + + On success the account moves to `ACTIVE`; on an invalid signature it + moves to `UNVERIFIED` (start a new challenge to retry). `LIVENESS` + challenges complete asynchronously and never use this endpoint — their + outcome is delivered via `EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by + polling the account. + operationId: verifyPlatformExternalAccountOwnership + tags: + - External Accounts + security: + - BasicAuth: [] + parameters: + - name: externalAccountId + in: path + description: External account ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/OwnershipVerifyRequest.yaml + responses: + '200': + description: >- + Signature valid; the updated external account is returned with + `status: ACTIVE`. + content: + application/json: + schema: + $ref: ../../components/schemas/external_accounts/ExternalAccount.yaml + '400': + description: >- + Invalid or expired signature. The account moves to `UNVERIFIED`; + start a new challenge to retry. + 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: External account not found + content: + application/json: + schema: + $ref: ../../components/schemas/errors/Error404.yaml + '409': + description: >- + No `WALLET_SIGNATURE` challenge is outstanding for this external + account. + 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/webhooks/external-account-status.yaml b/openapi/webhooks/external-account-status.yaml index cb53af481..d528ec031 100644 --- a/openapi/webhooks/external-account-status.yaml +++ b/openapi/webhooks/external-account-status.yaml @@ -3,8 +3,11 @@ post: description: > Webhook that is called whenever the status of an external account changes, for any transition between statuses (`PENDING`, `ACTIVE`, `UNDER_REVIEW`, - `INACTIVE`, `PENDING_OWNERSHIP_VERIFICATION`) — for example when an account - under review becomes active, or when ownership verification completes. + `INACTIVE`, `PENDING_OWNERSHIP_VERIFICATION`, `UNVERIFIED`) — for example + when an account under review becomes active, when ownership verification + completes (`PENDING_OWNERSHIP_VERIFICATION` → `ACTIVE`), or when a + verification attempt fails (`PENDING_OWNERSHIP_VERIFICATION` → + `UNVERIFIED`). This endpoint should be implemented by clients of the Grid API. @@ -48,7 +51,7 @@ post: schema: $ref: ../components/schemas/webhooks/ExternalAccountStatusWebhook.yaml examples: - statusUpdated: + verificationSucceeded: summary: A wallet account became active after ownership verification value: id: Webhook:019542f5-b3e7-1d02-0000-000000000042 @@ -63,6 +66,21 @@ post: accountInfo: accountType: ETHEREUM_WALLET address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' + verificationFailed: + summary: A liveness check failed; a new challenge is needed to retry + value: + id: Webhook:019542f5-b3e7-1d02-0000-000000000043 + type: EXTERNAL_ACCOUNT.STATUS_UPDATED + timestamp: '2025-08-15T14:45:00Z' + data: + id: ExternalAccount:e85dcbd6-dced-4ec4-b756-3c3a9ea3d965 + customerId: Customer:da459a29-1fb7-41ce-a4cb-eb3a3c9fd7a7 + status: UNVERIFIED + currency: USDC + ownershipType: FIRST_PARTY + accountInfo: + accountType: ETHEREUM_WALLET + address: '0xAbCDEF1234567890aBCdEf1234567890ABcDef12' responses: '200': description: > diff --git a/openapi/webhooks/ownership-verification.yaml b/openapi/webhooks/ownership-verification.yaml deleted file mode 100644 index 4a507f139..000000000 --- a/openapi/webhooks/ownership-verification.yaml +++ /dev/null @@ -1,109 +0,0 @@ -post: - summary: Ownership verification status change - description: > - Webhook that is called when the status of an ownership verification - 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. - - - ### Event types - - - `OWNERSHIP_VERIFICATION.PENDING_REVIEW` — Fired when a submitted - ownership verification enters review. The `data` payload contains the full - ownership verification object. - - - `OWNERSHIP_VERIFICATION.VERIFIED` — Fired when ownership of the external - account has been verified. The `data` payload contains the full ownership - verification object. - - - `OWNERSHIP_VERIFICATION.FAILED` — Fired when an ownership verification - attempt fails; start a new verification to retry. The `data` payload - contains the full ownership verification object. - - - operationId: ownershipVerificationWebhook - tags: - - Webhooks - security: - - WebhookSignature: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: ../components/schemas/webhooks/OwnershipVerificationWebhook.yaml - examples: - verified: - summary: Ownership of a self-custody wallet has been verified - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000040 - type: OWNERSHIP_VERIFICATION.VERIFIED - timestamp: '2025-08-15T15:10:00Z' - data: - id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000001 - externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: WALLET_SIGNATURE - status: VERIFIED - messageToSign: 'I verify that I control this wallet. Nonce: 019542f5-b3e7-1d02' - expiresAt: '2025-08-15T15:32:00Z' - createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T15:10:00Z' - failed: - summary: An ownership verification attempt failed - value: - id: Webhook:019542f5-b3e7-1d02-0000-000000000041 - type: OWNERSHIP_VERIFICATION.FAILED - timestamp: '2025-08-15T15:10:00Z' - data: - id: OwnershipVerification:019542f5-b3e7-1d02-0000-000000000002 - externalAccountId: ExternalAccount:019542f5-b3e7-1d02-0000-000000000001 - method: LIVENESS - status: FAILED - verificationLink: https://verify.example.com/session/019542f5-b3e7-1d02 - token: eyJhbGciOiJIUzI1NiJ9.example - expiresAt: '2025-08-15T15:32:00Z' - createdAt: '2025-08-15T15:02:00Z' - updatedAt: '2025-08-15T15:10:00Z' - responses: - '200': - description: > - Webhook received successfully - '400': - description: Bad request - content: - application/json: - schema: - $ref: ../components/schemas/errors/Error400.yaml - '401': - description: Unauthorized - Signature validation failed - content: - application/json: - schema: - $ref: ../components/schemas/errors/Error401.yaml - '409': - description: Conflict - Webhook has already been processed (duplicate id) - content: - application/json: - schema: - $ref: ../components/schemas/errors/Error409.yaml From db6a04661bb0be8aef66b2345a2e1b65ccae1faa Mon Sep 17 00:00:00 2001 From: shreyav Date: Thu, 13 Aug 2026 23:32:47 +0000 Subject: [PATCH 11/11] Tighten external account status docs for the API reference --- mintlify/openapi.yaml | 18 +++++------------- openapi.yaml | 18 +++++------------- .../ExternalAccountStatus.yaml | 18 +++++------------- 3 files changed, 15 insertions(+), 39 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 894642231..7a72ab91d 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -17236,19 +17236,11 @@ components: description: | Status of an external account. - `PENDING_OWNERSHIP_VERIFICATION` applies to crypto wallet accounts whose - ownership must be verified before the account can be used without - restriction — for example, under the EU Travel Rule or similar requirements - in other regions. It covers both "verification not yet started" and - "verification in progress" (e.g. a liveness check underway). While in this - status, the account can be used for transfers below regulatory thresholds. - - `UNVERIFIED` means the most recent ownership verification attempt failed. - The account keeps the same below-threshold capabilities as - `PENDING_OWNERSHIP_VERIFICATION`; start a new challenge to retry, which - returns the account to `PENDING_OWNERSHIP_VERIFICATION`. - - Completing ownership verification moves the account to `ACTIVE`. + `PENDING_OWNERSHIP_VERIFICATION`: the wallet's ownership must be verified + (e.g. under the EU Travel Rule) before unrestricted use; transfers below + regulatory thresholds are still allowed. `UNVERIFIED`: the most recent + verification attempt failed; start a new challenge to retry. Successful + verification moves the account to `ACTIVE`. OwnershipType: type: string enum: diff --git a/openapi.yaml b/openapi.yaml index 894642231..7a72ab91d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -17236,19 +17236,11 @@ components: description: | Status of an external account. - `PENDING_OWNERSHIP_VERIFICATION` applies to crypto wallet accounts whose - ownership must be verified before the account can be used without - restriction — for example, under the EU Travel Rule or similar requirements - in other regions. It covers both "verification not yet started" and - "verification in progress" (e.g. a liveness check underway). While in this - status, the account can be used for transfers below regulatory thresholds. - - `UNVERIFIED` means the most recent ownership verification attempt failed. - The account keeps the same below-threshold capabilities as - `PENDING_OWNERSHIP_VERIFICATION`; start a new challenge to retry, which - returns the account to `PENDING_OWNERSHIP_VERIFICATION`. - - Completing ownership verification moves the account to `ACTIVE`. + `PENDING_OWNERSHIP_VERIFICATION`: the wallet's ownership must be verified + (e.g. under the EU Travel Rule) before unrestricted use; transfers below + regulatory thresholds are still allowed. `UNVERIFIED`: the most recent + verification attempt failed; start a new challenge to retry. Successful + verification moves the account to `ACTIVE`. OwnershipType: type: string enum: diff --git a/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml b/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml index 2b98bc325..d4a2bb3a6 100644 --- a/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml +++ b/openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml @@ -9,16 +9,8 @@ enum: description: | Status of an external account. - `PENDING_OWNERSHIP_VERIFICATION` applies to crypto wallet accounts whose - ownership must be verified before the account can be used without - restriction — for example, under the EU Travel Rule or similar requirements - in other regions. It covers both "verification not yet started" and - "verification in progress" (e.g. a liveness check underway). While in this - status, the account can be used for transfers below regulatory thresholds. - - `UNVERIFIED` means the most recent ownership verification attempt failed. - The account keeps the same below-threshold capabilities as - `PENDING_OWNERSHIP_VERIFICATION`; start a new challenge to retry, which - returns the account to `PENDING_OWNERSHIP_VERIFICATION`. - - Completing ownership verification moves the account to `ACTIVE`. + `PENDING_OWNERSHIP_VERIFICATION`: the wallet's ownership must be verified + (e.g. under the EU Travel Rule) before unrestricted use; transfers below + regulatory thresholds are still allowed. `UNVERIFIED`: the most recent + verification attempt failed; start a new challenge to retry. Successful + verification moves the account to `ACTIVE`.