Skip to content

feat: add external account ownership verification - #806

Merged
shreyav merged 13 commits into
mainfrom
shreyav/ownership-verification
Aug 13, 2026
Merged

feat: add external account ownership verification#806
shreyav merged 13 commits into
mainfrom
shreyav/ownership-verification

Conversation

@shreyav

@shreyav shreyav commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds ownership verification for FIRST_PARTY self-custody crypto wallet external accounts, as account-scoped challenge/verify actions (per review feedback — this replaces the earlier standalone /ownership-verifications resource design).

Endpoints (on both /customers/external-accounts/{id} and /platform/external-accounts/{id})

  • POST …/challenge — start (or restart) verification with a chosen method:

    • WALLET_SIGNATURE → returns a messageToSign
    • LIVENESS → returns a hosted verificationLink (+ optional embed token, mirroring the KYC link contract)

    Calling challenge again abandons any in-flight challenge — that's how you retry after a failure, replace an expired challenge, or switch methods. An UNVERIFIED account returns to PENDING_OWNERSHIP_VERIFICATION.

  • POST …/verify — complete a WALLET_SIGNATURE challenge synchronously by submitting the signature (signatureScheme: bip137/electrum for Bitcoin). LIVENESS completes 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 to PENDING_OWNERSHIP_VERIFICATION.
  • Success → 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, mirrors INTERNAL_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

  • Renamed WALLET_VERIFICATION_REQUIRED to EXTERNAL_ACCOUNT_VERIFICATION_REQUIRED; removed OWNERSHIP_TYPE_REQUIRED, THIRD_PARTY_TRANSFER_LIMIT_EXCEEDED, and LIGHTNING_TRANSFER_LIMIT_EXCEEDED.

Other

  • Expanded ownershipType docs (generalized beyond EU Travel Rule).
  • Stainless: challenge/verify methods on the customer and platform external-accounts subresources + challenge/request models.

Testing

make build bundles cleanly; redocly lint and spectral lint match the pre-existing baseline on main exactly (no new findings).

🤖 Generated with Claude Code

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>
@mintlify

mintlify Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview Aug 6, 2026, 4:56 AM

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Aug 13, 2026 11:32pm
grid-wallet-demo Ignored Ignored Preview Aug 13, 2026 11:32pm

Request Review

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

cli

docs(api): clarify ownership-type parameter usage in external accounts

go

feat(api): add ownership verification webhook event and status field to external accounts

kotlin

feat(api): add ownershipVerificationStatus field and webhook events to external accounts

openapi

feat(api): add ownership verification endpoints/webhook to external accounts, update types

php

feat(api): add ownershipVerificationStatus to ExternalAccount, ExternalAccount webhook

python

feat(api): add ownership verification webhook event and status field to external accounts

ruby

feat(api): add ownership verification status field and webhook events to external accounts

typescript

feat(api): add ExternalAccountWebhookEvent, ownershipVerificationStatus to external accounts
⚠️ grid-openapi studio · code

Your SDK build had at least one "warning" diagnostic.
generate ⚠️

grid-ruby studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅build ✅lint ✅test ✅

⚠️ grid-go studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@ee28fa78b3604d498d8f9ba46473e418235aef4a
⚠️ grid-kotlin studio · code

Your SDK build had a failure in the test CI job, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ❗

grid-typescript studio · conflict

Your SDK build had at least one note diagnostic.

⚠️ grid-python studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/0f5b8d09a45e337b64b1c17158f9ea8efa51e55a/grid-0.0.1-py3-none-any.whl
grid-php studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅lint ✅test ✅

⚠️ grid-cli studio · code

Your SDK build had a failure in the build CI job, which is a regression from the base state.
generate ⚠️build ❗lint ❗test ❗


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-08-13 23:55:12 UTC

…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-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds wallet ownership-verification creation, confirmation, retrieval, listing, lifecycle schemas, and related external-account and webhook contracts.

  • Adds WALLET_SIGNATURE and LIVENESS verification methods with lifecycle states.
  • Adds ownership-verification and external-account status webhooks.
  • Registers the new API surface for Stainless SDK generation.
  • Extends external-account status and ownership metadata for regulated self-custody wallets.

Confidence Score: 4/5

The 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

Important Files Changed

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]
Loading
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

Comment thread openapi/paths/ownership_verifications/ownership-verifications.yaml Outdated
Comment thread openapi/openapi.yaml Outdated
$ref: paths/platform/platform_external_accounts.yaml
/platform/external-accounts/{externalAccountId}:
$ref: paths/platform/platform_external_accounts_{externalAccountId}.yaml
/ownership-verifications:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I tried to follow the KYC verification pattern... open to either way

Comment thread openapi/components/schemas/external_accounts/ExternalAccountStatus.yaml Outdated
# 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.
@shreyav
shreyav merged commit 53c71fe into main Aug 13, 2026
11 checks passed
@shreyav
shreyav deleted the shreyav/ownership-verification branch August 13, 2026 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants