diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml
index edd1c5018..f037a2673 100644
--- a/mintlify/openapi.yaml
+++ b/mintlify/openapi.yaml
@@ -2580,12 +2580,16 @@ paths:
asynchronously. The outcome is delivered via
`EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account.
- Calling this endpoint again abandons any in-flight challenge and issues a
- new one with the requested method — use it to retry after a failed
- attempt, to replace an expired challenge, or to switch methods. An
- `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a
- new challenge is issued.
+ While a `WALLET_SIGNATURE` challenge is outstanding, calling this endpoint
+ again returns that same challenge, with the same `messageToSign` and
+ `expiresAt`. Reloading a signing page therefore does not invalidate the
+ message the user is about to sign. You get a new challenge once the
+ outstanding one expires or its attempt fails.
+ Requesting `LIVENESS` always starts a liveness attempt, even while a
+ signature challenge is outstanding.
+
+ An `UNVERIFIED` account stays `UNVERIFIED` until a retry succeeds.
Completing ownership verification moves the account to `ACTIVE`.
operationId: createExternalAccountOwnershipChallenge
tags:
@@ -2932,12 +2936,16 @@ paths:
asynchronously. The outcome is delivered via
`EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account.
- Calling this endpoint again abandons any in-flight challenge and issues a
- new one with the requested method — use it to retry after a failed
- attempt, to replace an expired challenge, or to switch methods. An
- `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a
- new challenge is issued.
+ While a `WALLET_SIGNATURE` challenge is outstanding, calling this endpoint
+ again returns that same challenge, with the same `messageToSign` and
+ `expiresAt`. Reloading a signing page therefore does not invalidate the
+ message the user is about to sign. You get a new challenge once the
+ outstanding one expires or its attempt fails.
+
+ Requesting `LIVENESS` always starts a liveness attempt, even while a
+ signature challenge is outstanding.
+ An `UNVERIFIED` account stays `UNVERIFIED` until a retry succeeds.
Completing ownership verification moves the account to `ACTIVE`.
operationId: createPlatformExternalAccountOwnershipChallenge
tags:
diff --git a/mintlify/platform-overview/core-concepts/account-model.mdx b/mintlify/platform-overview/core-concepts/account-model.mdx
index c6559d1e1..242069a89 100644
--- a/mintlify/platform-overview/core-concepts/account-model.mdx
+++ b/mintlify/platform-overview/core-concepts/account-model.mdx
@@ -209,11 +209,63 @@ PENDING → ACTIVE
- **PENDING**: Created, undergoing verification/screening
- **ACTIVE**: Verified, ready for transactions
+- **PENDING_OWNERSHIP_VERIFICATION**: The customer must prove they control this self-custody wallet
+- **UNVERIFIED**: The last ownership attempt failed. Start a new challenge to retry
- **INACTIVE**: Disabled (can be reactivated)
- **UNDER_REVIEW**: Additional review required
You'll receive `INTERNAL_ACCOUNT.BALANCE_UPDATED` webhooks as balance changes, and `INTERNAL_ACCOUNT.STATUS_UPDATED` webhooks when account status changes (e.g., ACTIVE to FROZEN).
+### Ownership Verification
+
+Some regions require the owner of a self-custody wallet to prove they control it (e.g. the EU). Register the wallet with `ownershipType` set to `FIRST_PARTY` and it starts at `PENDING_OWNERSHIP_VERIFICATION`.
+
+The status is not a block: transfers below the regulatory threshold go through while a wallet is pending. Larger transfers need verification to be complete first.
+
+Start a challenge on the account, choosing how the owner will prove control:
+
+```bash
+curl -X POST https://api.lightspark.com/grid/2025-10-13/customers/external-accounts/ExternalAccount:abc123/challenge \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{"method": "WALLET_SIGNATURE"}'
+```
+
+**`WALLET_SIGNATURE`** returns a message for the wallet to sign:
+
+```json
+{
+ "method": "WALLET_SIGNATURE",
+ "messageToSign": "I am verifying ownership of the wallet address 7Ub1FK9aE5MsQBYfEdxSXorWeCGq9xPgPygL9XjA3KZz as a361e175-69b4-4bfb-9ceb-acb6ea672cf0. This message was signed on 04/09/2026 to confirm my control over this wallet.",
+ "expiresAt": "2026-09-06T00:00:00Z"
+}
+```
+
+Submit the signature to complete verification synchronously. The response is the external account, with `status` set to `ACTIVE` on success:
+
+```bash
+curl -X POST https://api.lightspark.com/grid/2025-10-13/customers/external-accounts/ExternalAccount:abc123/verify \
+ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
+ -H "Content-Type: application/json" \
+ -d '{"signature": "5tCtPdm7CnT3YyYChEdiiEH5..."}'
+```
+
+
+ Sign `messageToSign` exactly as returned. It is matched character for character, and the date it carries is only accepted for yesterday, today, and tomorrow in UTC. Never rebuild the message yourself.
+
+
+**`LIVENESS`** returns a hosted `verificationLink` (and an embed `token` where supported). The owner completes a biometric flow and verification finishes asynchronously, so watch for `EXTERNAL_ACCOUNT.STATUS_UPDATED` or poll the account.
+
+
+ Calling the challenge endpoint again while a signature challenge is outstanding returns that same challenge, so reloading a signing page does not invalidate the message the owner is about to sign. A refused signature moves the account to `UNVERIFIED`, where it stays until a retry succeeds.
+
+
+Verification applies only to first-party self-custody wallets. A wallet held at an exchange is declared by setting `vaspName` inside `accountInfo`, and skips verification because the keys belong to the exchange. `THIRD_PARTY` wallets skip it too.
+
+
+ `vaspName` belongs inside `accountInfo`. At the top level it is ignored, and the wallet is treated as self-custody.
+
+
### Using External Accounts
diff --git a/mintlify/ramps/accounts/external-accounts.mdx b/mintlify/ramps/accounts/external-accounts.mdx
index a64ade151..a142e04f4 100644
--- a/mintlify/ramps/accounts/external-accounts.mdx
+++ b/mintlify/ramps/accounts/external-accounts.mdx
@@ -172,18 +172,30 @@ curl -X GET 'https://api.lightspark.com/grid/2025-10-13/customers/external-accou
External accounts move through verification states:
-| Status | Description | Can Use for Conversions |
-| ---------- | ------------------------ | ----------------------- |
-| `PENDING` | Verification in progress | ❌ |
-| `ACTIVE` | Verified and ready | ✅ |
-| `UNDER_REVIEW` | Additional review required | ❌ |
-| `INACTIVE` | Manually disabled | ❌ |
+| Status | Description | Can use for conversions |
+| -------------------------------- | ----------------------------------------------------------------- | ------------------------------ |
+| `PENDING` | Verification in progress | No |
+| `ACTIVE` | Verified and ready | Yes |
+| `PENDING_OWNERSHIP_VERIFICATION` | The customer must prove they control this self-custody wallet | Below the regulatory threshold |
+| `UNVERIFIED` | The last ownership attempt failed. Start a new challenge to retry | Below the regulatory threshold |
+| `UNDER_REVIEW` | Additional review required | No |
+| `INACTIVE` | Manually disabled | No |
Spark wallet accounts are immediately `ACTIVE`. Bank accounts may require
verification (typically instant to a few hours).
+
+ A self-custody wallet registered with `ownershipType` set to `FIRST_PARTY`
+ starts at `PENDING_OWNERSHIP_VERIFICATION` in regions that require the owner to
+ prove they control it (e.g. the EU). It is not blocked: conversions below the
+ regulatory threshold go through while the wallet is pending. To clear the
+ status, start a challenge with
+ `POST /customers/external-accounts/{externalAccountId}/challenge` and complete
+ it with either a wallet signature or a liveness check.
+
+
## Best practices for ramps
diff --git a/mintlify/snippets/external-accounts.mdx b/mintlify/snippets/external-accounts.mdx
index 107545b25..946b97af8 100644
--- a/mintlify/snippets/external-accounts.mdx
+++ b/mintlify/snippets/external-accounts.mdx
@@ -1970,14 +1970,18 @@ For business beneficiaries, the required fields vary by destination currency:
## Account status
-Beneficiary data may be reviewed for risk and compliance. Only `ACTIVE` accounts can receive payments. Updates to account data may trigger account re-review.
-
-| Status | Description |
-| -------------- | ----------------------------------- |
-| `PENDING` | Created, awaiting verification |
-| `ACTIVE` | Verified and ready for transactions |
-| `UNDER_REVIEW` | Additional review required |
-| `INACTIVE` | Disabled, cannot be used |
+Beneficiary data may be reviewed for risk and compliance. Only `ACTIVE` accounts can receive payments, except that a self-custody wallet still owed ownership verification can receive transfers below the regulatory threshold. Updates to account data may trigger account re-review.
+
+| Status | Description |
+| -------------------------------- | --------------------------------------------------------------------------------- |
+| `PENDING` | Created, awaiting verification |
+| `ACTIVE` | Verified and ready for transactions |
+| `PENDING_OWNERSHIP_VERIFICATION` | The customer must prove they control this self-custody wallet |
+| `UNVERIFIED` | The last ownership attempt failed. Start a new challenge to retry |
+| `UNDER_REVIEW` | Additional review required |
+| `INACTIVE` | Disabled, cannot be used |
+
+A wallet reaches `PENDING_OWNERSHIP_VERIFICATION` when you register it with `ownershipType` set to `FIRST_PARTY` in a region that requires the owner to prove they control it (e.g. the EU). To clear it, start a challenge with `POST /customers/external-accounts/{externalAccountId}/challenge` and complete it with either a wallet signature or a liveness check. Both statuses allow transfers below the regulatory threshold, so a pending wallet is not blocked outright.
## Listing external accounts
diff --git a/openapi.yaml b/openapi.yaml
index edd1c5018..f037a2673 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -2580,12 +2580,16 @@ paths:
asynchronously. The outcome is delivered via
`EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account.
- Calling this endpoint again abandons any in-flight challenge and issues a
- new one with the requested method — use it to retry after a failed
- attempt, to replace an expired challenge, or to switch methods. An
- `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a
- new challenge is issued.
+ While a `WALLET_SIGNATURE` challenge is outstanding, calling this endpoint
+ again returns that same challenge, with the same `messageToSign` and
+ `expiresAt`. Reloading a signing page therefore does not invalidate the
+ message the user is about to sign. You get a new challenge once the
+ outstanding one expires or its attempt fails.
+ Requesting `LIVENESS` always starts a liveness attempt, even while a
+ signature challenge is outstanding.
+
+ An `UNVERIFIED` account stays `UNVERIFIED` until a retry succeeds.
Completing ownership verification moves the account to `ACTIVE`.
operationId: createExternalAccountOwnershipChallenge
tags:
@@ -2932,12 +2936,16 @@ paths:
asynchronously. The outcome is delivered via
`EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account.
- Calling this endpoint again abandons any in-flight challenge and issues a
- new one with the requested method — use it to retry after a failed
- attempt, to replace an expired challenge, or to switch methods. An
- `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a
- new challenge is issued.
+ While a `WALLET_SIGNATURE` challenge is outstanding, calling this endpoint
+ again returns that same challenge, with the same `messageToSign` and
+ `expiresAt`. Reloading a signing page therefore does not invalidate the
+ message the user is about to sign. You get a new challenge once the
+ outstanding one expires or its attempt fails.
+
+ Requesting `LIVENESS` always starts a liveness attempt, even while a
+ signature challenge is outstanding.
+ An `UNVERIFIED` account stays `UNVERIFIED` until a retry succeeds.
Completing ownership verification moves the account to `ACTIVE`.
operationId: createPlatformExternalAccountOwnershipChallenge
tags:
diff --git a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_challenge.yaml b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_challenge.yaml
index abc37e643..364becae7 100644
--- a/openapi/paths/customers/customers_external_accounts_{externalAccountId}_challenge.yaml
+++ b/openapi/paths/customers/customers_external_accounts_{externalAccountId}_challenge.yaml
@@ -14,12 +14,16 @@ post:
asynchronously. The outcome is delivered via
`EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account.
- Calling this endpoint again abandons any in-flight challenge and issues a
- new one with the requested method — use it to retry after a failed
- attempt, to replace an expired challenge, or to switch methods. An
- `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a
- new challenge is issued.
+ While a `WALLET_SIGNATURE` challenge is outstanding, calling this endpoint
+ again returns that same challenge, with the same `messageToSign` and
+ `expiresAt`. Reloading a signing page therefore does not invalidate the
+ message the user is about to sign. You get a new challenge once the
+ outstanding one expires or its attempt fails.
+ Requesting `LIVENESS` always starts a liveness attempt, even while a
+ signature challenge is outstanding.
+
+ An `UNVERIFIED` account stays `UNVERIFIED` until a retry succeeds.
Completing ownership verification moves the account to `ACTIVE`.
operationId: createExternalAccountOwnershipChallenge
tags:
diff --git a/openapi/paths/platform/platform_external_accounts_{externalAccountId}_challenge.yaml b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_challenge.yaml
index 532decc60..a4ab543b2 100644
--- a/openapi/paths/platform/platform_external_accounts_{externalAccountId}_challenge.yaml
+++ b/openapi/paths/platform/platform_external_accounts_{externalAccountId}_challenge.yaml
@@ -14,12 +14,16 @@ post:
asynchronously. The outcome is delivered via
`EXTERNAL_ACCOUNT.STATUS_UPDATED` webhooks or by polling the account.
- Calling this endpoint again abandons any in-flight challenge and issues a
- new one with the requested method — use it to retry after a failed
- attempt, to replace an expired challenge, or to switch methods. An
- `UNVERIFIED` account returns to `PENDING_OWNERSHIP_VERIFICATION` when a
- new challenge is issued.
+ While a `WALLET_SIGNATURE` challenge is outstanding, calling this endpoint
+ again returns that same challenge, with the same `messageToSign` and
+ `expiresAt`. Reloading a signing page therefore does not invalidate the
+ message the user is about to sign. You get a new challenge once the
+ outstanding one expires or its attempt fails.
+ Requesting `LIVENESS` always starts a liveness attempt, even while a
+ signature challenge is outstanding.
+
+ An `UNVERIFIED` account stays `UNVERIFIED` until a retry succeeds.
Completing ownership verification moves the account to `ACTIVE`.
operationId: createPlatformExternalAccountOwnershipChallenge
tags: