feat: add external account ownership verification - #806
Conversation
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 <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
✱ Stainless preview builds for gridThis PR will update the cli go kotlin openapi php python ruby typescript ✅ grid-ruby studio · code
|
…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 <noreply@anthropic.com>
…ions 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 <noreply@anthropic.com>
…less 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 <noreply@anthropic.com>
# Conflicts: # mintlify/openapi.yaml # openapi.yaml # openapi/components/schemas/errors/Error400.yaml
Greptile SummaryThe PR adds wallet ownership-verification creation, confirmation, retrieval, listing, lifecycle schemas, and related external-account and webhook contracts.
Confidence Score: 4/5The PR appears safe to merge after correcting the non-blocking LIVENESS documentation inconsistency around the optional embed token. The API surface is coherently wired through source schemas, generated bundles, webhooks, and Stainless, but the create-endpoint description can mislead integrations into assuming every liveness provider returns an embed token. Files Needing Attention: openapi/paths/ownership_verifications/ownership-verifications.yaml
|
| Filename | Overview |
|---|---|
| openapi/paths/ownership_verifications/ownership-verifications.yaml | Defines create and list operations; the LIVENESS description incorrectly presents the provider-dependent token as guaranteed. |
| openapi/paths/ownership_verifications/ownership-verifications_{verificationId}_confirm.yaml | Defines synchronous wallet-signature confirmation and its pending, expiry, and method constraints. |
| openapi/components/schemas/ownership_verifications/OwnershipVerification.yaml | Introduces a method-discriminated union for wallet-signature and liveness verification resources. |
| openapi/components/schemas/ownership_verifications/OwnershipVerificationConfirmRequest.yaml | Defines chain-specific signature input, including the signer address and optional Bitcoin signing scheme. |
| openapi/components/schemas/ownership_verifications/LivenessOwnershipVerification.yaml | Models hosted liveness sessions and correctly makes the provider-specific embed token optional. |
| openapi/components/schemas/webhooks/OwnershipVerificationWebhook.yaml | Adds signed webhook payload contracts for pending-review, verified, and failed ownership-verification states. |
| openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml | Adds the regulated PENDING_OWNERSHIP_VERIFICATION account state and documents its transfer restrictions. |
| .stainless/stainless.yml | Registers the ownership-verification resource, models, and four operations for SDK generation. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Create ownership verification] --> B{Method}
B -->|WALLET_SIGNATURE| C[Receive messageToSign]
C --> D[Wallet signs message]
D --> E[Confirm verification]
B -->|LIVENESS| F[Open verificationLink or use optional token]
F --> G[Hosted biometric flow]
E --> H{Result}
G --> H
H -->|Pending review| I[Webhook or polling]
H -->|Verified| J[External account becomes ACTIVE]
H -->|Failed| K[Start a new verification]
Prompt To Fix All With AI
### Issue 1
openapi/paths/ownership_verifications/ownership-verifications.yaml:11-12
**Liveness token documented as guaranteed**
For providers without direct SDK integration, `token` is omitted, but this endpoint says every LIVENESS response includes one, leading integrators to initialize an embedded flow without the required token instead of using `verificationLink`.
```suggestion
- `LIVENESS` — the response includes a `verificationLink` and may include a
`token` for embedding; the user completes a hosted biometric flow and
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "Make liveness verification token optiona..." | Re-trigger Greptile
| $ref: paths/platform/platform_external_accounts.yaml | ||
| /platform/external-accounts/{externalAccountId}: | ||
| $ref: paths/platform/platform_external_accounts_{externalAccountId}.yaml | ||
| /ownership-verifications: |
There was a problem hiding this comment.
Did you consider scoping this to the external account resource? I know it makes the path long, but it does seem pretty tied to the account... /customers/external-accounts/{externalAccountId}/verification or something?
There was a problem hiding this comment.
hmm I tried to follow the KYC verification pattern... open to either way
# Conflicts: # mintlify/openapi.yaml # openapi.yaml # openapi/components/schemas/errors/Error400.yaml
…dpoints 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.
Summary
Adds ownership verification for
FIRST_PARTYself-custody crypto wallet external accounts, as account-scoped challenge/verify actions (per review feedback — this replaces the earlier standalone/ownership-verificationsresource design).Endpoints (on both
/customers/external-accounts/{id}and/platform/external-accounts/{id})POST …/challenge— start (or restart) verification with a chosenmethod:WALLET_SIGNATURE→ returns amessageToSignLIVENESS→ returns a hostedverificationLink(+ optional embedtoken, mirroring the KYC link contract)Calling
challengeagain abandons any in-flight challenge — that's how you retry after a failure, replace an expired challenge, or switch methods. AnUNVERIFIEDaccount returns toPENDING_OWNERSHIP_VERIFICATION.POST …/verify— complete aWALLET_SIGNATUREchallenge synchronously by submitting the signature (signatureScheme:bip137/electrumfor Bitcoin).LIVENESScompletes asynchronously and never calls this endpoint.Nothing verification-related is collected on the account create request.
External account statuses
PENDING_OWNERSHIP_VERIFICATION— ownership must be verified before unrestricted use (e.g., under EU Travel Rule or similar regimes). Covers both "not yet started" and "in progress" (liveness underway). Usable below regulatory thresholds.UNVERIFIED(new) — the most recent verification attempt failed; same below-threshold capabilities; a new challenge returns it toPENDING_OWNERSHIP_VERIFICATION.ACTIVE.Both statuses only ever appear on crypto wallet accounts on platforms with counterparty requirements, so existing integrations are unaffected.
Webhooks
EXTERNAL_ACCOUNT.STATUS_UPDATED(new, mirrorsINTERNAL_ACCOUNT.STATUS_UPDATED) fires on any status transition and carries the full account. Because failed attempts are a status transition (→ UNVERIFIED), this single event covers the whole verification lifecycle — including async liveness failures — with no bespoke verification webhooks.Error codes
WALLET_VERIFICATION_REQUIREDtoEXTERNAL_ACCOUNT_VERIFICATION_REQUIRED; removedOWNERSHIP_TYPE_REQUIRED,THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED, andLIGHTNING_TRANSFER_LIMIT_EXCEEDED.Other
ownershipTypedocs (generalized beyond EU Travel Rule).challenge/verifymethods on the customer and platform external-accounts subresources + challenge/request models.Testing
make buildbundles cleanly;redocly lintandspectral lintmatch the pre-existing baseline onmainexactly (no new findings).🤖 Generated with Claude Code