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 9f0838e82..90eacd2e1 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 @@ -2633,6 +2635,248 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /ownership-verifications: + 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/OwnershipVerificationRequest' + responses: + '201': + description: Ownership verification created; the method-specific material is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerification' + '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. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + 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: + 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/OwnershipVerificationConfirmRequest' + responses: + '200': + description: Signature submitted; the updated ownership verification is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerification' + '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: Ownership verification not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The verification is not a `WALLET_SIGNATURE` verification in `PENDING` status. + 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 +10994,74 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + external-account-status: + 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. + 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/ExternalAccountStatusWebhook' + 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/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 @@ -10829,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-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 @@ -11841,6 +12239,7 @@ components: | 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 | | CARDHOLDER_KYC_NOT_APPROVED | The cardholder's KYC status is not `APPROVED`, so a card cannot be issued | + | 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 @@ -11883,6 +12282,7 @@ components: - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED - CARDHOLDER_KYC_NOT_APPROVED + - EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED message: type: string description: Error message @@ -16850,15 +17250,24 @@ 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: - 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 @@ -20170,6 +20579,203 @@ components: $ref: '#/components/schemas/OwnershipType' accountInfo: $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' + OwnershipVerificationState: + 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 + WalletSignatureOwnershipVerification: + 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: '#/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: 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: '#/components/schemas/OwnershipVerificationState' + 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' + 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 verification'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: + - bip137 + - electrum + default: bip137 + description: Bitcoin message-signing format. Defaults to `bip137`; use `electrum` for Electrum/Sparrow wallets. Ignored for non-Bitcoin chains. BeneficialOwnerListResponse: type: object required: @@ -24725,6 +25331,10 @@ 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 - INVITATION.CLAIMED @@ -24919,6 +25529,19 @@ components: enum: - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED + ExternalAccountStatusWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ExternalAccount' + type: + type: string + enum: + - EXTERNAL_ACCOUNT.STATUS_UPDATED VerificationWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' @@ -24936,6 +25559,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/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 { diff --git a/openapi.yaml b/openapi.yaml index 9f0838e82..90eacd2e1 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 @@ -2633,6 +2635,248 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' + /ownership-verifications: + 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/OwnershipVerificationRequest' + responses: + '201': + description: Ownership verification created; the method-specific material is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerification' + '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. + content: + application/json: + schema: + $ref: '#/components/schemas/Error409' + '500': + description: Internal service error + content: + 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: + 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/OwnershipVerificationConfirmRequest' + responses: + '200': + description: Signature submitted; the updated ownership verification is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnershipVerification' + '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: Ownership verification not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error404' + '409': + description: The verification is not a `WALLET_SIGNATURE` verification in `PENDING` status. + 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 +10994,74 @@ webhooks: application/json: schema: $ref: '#/components/schemas/Error409' + external-account-status: + 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. + 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/ExternalAccountStatusWebhook' + 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/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 @@ -10829,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-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 @@ -11841,6 +12239,7 @@ components: | 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 | | CARDHOLDER_KYC_NOT_APPROVED | The cardholder's KYC status is not `APPROVED`, so a card cannot be issued | + | 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 @@ -11883,6 +12282,7 @@ components: - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED - CARDHOLDER_KYC_NOT_APPROVED + - EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED message: type: string description: Error message @@ -16850,15 +17250,24 @@ 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: - 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 @@ -20170,6 +20579,203 @@ components: $ref: '#/components/schemas/OwnershipType' accountInfo: $ref: '#/components/schemas/ExternalAccountCreateInfoOneOf' + OwnershipVerificationState: + 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 + WalletSignatureOwnershipVerification: + 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: '#/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: 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: '#/components/schemas/OwnershipVerificationState' + 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' + 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 verification'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: + - bip137 + - electrum + default: bip137 + description: Bitcoin message-signing format. Defaults to `bip137`; use `electrum` for Electrum/Sparrow wallets. Ignored for non-Bitcoin chains. BeneficialOwnerListResponse: type: object required: @@ -24725,6 +25331,10 @@ 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 - INVITATION.CLAIMED @@ -24919,6 +25529,19 @@ components: enum: - INTERNAL_ACCOUNT.BALANCE_UPDATED - INTERNAL_ACCOUNT.STATUS_UPDATED + ExternalAccountStatusWebhook: + allOf: + - $ref: '#/components/schemas/BaseWebhook' + - type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/ExternalAccount' + type: + type: string + enum: + - EXTERNAL_ACCOUNT.STATUS_UPDATED VerificationWebhook: allOf: - $ref: '#/components/schemas/BaseWebhook' @@ -24936,6 +25559,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 007c10718..e71e3ad18 100644 --- a/openapi/components/schemas/errors/Error400.yaml +++ b/openapi/components/schemas/errors/Error400.yaml @@ -55,6 +55,7 @@ properties: | 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 | | CARDHOLDER_KYC_NOT_APPROVED | The cardholder's KYC status is not `APPROVED`, so a card cannot be issued | + | 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 @@ -97,6 +98,7 @@ properties: - STABLECOIN_PROVIDER_ACCOUNT_REVOKED - STABLECOIN_PROVIDER_ACCOUNT_SELECTION_REQUIRED - CARDHOLDER_KYC_NOT_APPROVED + - EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED message: type: string description: Error message 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/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/ownership_verifications/LivenessOwnershipVerification.yaml b/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml new file mode 100644 index 000000000..b565fc23c --- /dev/null +++ b/openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml @@ -0,0 +1,61 @@ +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 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/ownership_verifications/OwnershipVerificationConfirmRequest.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml new file mode 100644 index 000000000..3f275e2de --- /dev/null +++ b/openapi/components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml @@ -0,0 +1,27 @@ +type: object +description: >- + Completes a `WALLET_SIGNATURE` ownership verification by submitting the + signature the wallet produced for the verification'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: + - 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/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/ownership_verifications/OwnershipVerificationMethod.yaml b/openapi/components/schemas/ownership_verifications/OwnershipVerificationMethod.yaml new file mode 100644 index 000000000..6211b5928 --- /dev/null +++ b/openapi/components/schemas/ownership_verifications/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/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/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 fab72c524..3844b5725 100644 --- a/openapi/components/schemas/webhooks/WebhookType.yaml +++ b/openapi/components/schemas/webhooks/WebhookType.yaml @@ -26,6 +26,10 @@ 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 - INVITATION.CLAIMED diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index b6143351f..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 @@ -193,6 +197,12 @@ paths: $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 /beneficial-owners: $ref: paths/beneficial-owners/beneficial_owners.yaml /beneficial-owners/{beneficialOwnerId}: @@ -389,8 +399,12 @@ webhooks: $ref: webhooks/customer-update.yaml internal-account-status: $ref: webhooks/internal-account-status.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/ownership_verifications/ownership-verifications.yaml b/openapi/paths/ownership_verifications/ownership-verifications.yaml new file mode 100644 index 000000000..3625a972d --- /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 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 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/webhooks/external-account-status.yaml b/openapi/webhooks/external-account-status.yaml new file mode 100644 index 000000000..cb53af481 --- /dev/null +++ b/openapi/webhooks/external-account-status.yaml @@ -0,0 +1,87 @@ +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. + + 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/ownership-verification.yaml b/openapi/webhooks/ownership-verification.yaml new file mode 100644 index 000000000..4a507f139 --- /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-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