From a540549f4f7e7e74ea03447e91e52e9ba193f79e Mon Sep 17 00:00:00 2001 From: shreyav Date: Fri, 4 Sep 2026 13:43:40 -0700 Subject: [PATCH 1/3] docs: correct the ownership challenge behavior and list its account statuses Two claims on the challenge endpoints did not match what the API does, in both the customer and platform trees: - "Calling this endpoint again abandons any in-flight challenge". It returns the outstanding challenge unchanged instead, with the same messageToSign and expiresAt, so reloading a signing page does not invalidate the message the user is about to sign. - "An UNVERIFIED account returns to PENDING_OWNERSHIP_VERIFICATION when a new challenge is issued". It stays UNVERIFIED until a retry succeeds. Both were verified against the dev Striga platform. The endpoint descriptions now state the actual behavior, and note that requesting LIVENESS starts a liveness attempt even while a signature challenge is outstanding. The account status tables listed neither PENDING_OWNERSHIP_VERIFICATION nor UNVERIFIED, and the ramps table implied that any status other than ACTIVE cannot transact. Transfers below the regulatory threshold go through while a wallet is pending, so that table told integrators to build a block Grid does not impose. Both tables now carry the two statuses and say what they allow. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NrRvhfq3vabYreWe8yvFD1 --- mintlify/openapi.yaml | 28 ++++++++++++------- mintlify/ramps/accounts/external-accounts.mdx | 24 ++++++++++++---- mintlify/snippets/external-accounts.mdx | 20 +++++++------ openapi.yaml | 28 ++++++++++++------- ...ccounts_{externalAccountId}_challenge.yaml | 14 ++++++---- ...ccounts_{externalAccountId}_challenge.yaml | 14 ++++++---- 6 files changed, 84 insertions(+), 44 deletions(-) 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/ramps/accounts/external-accounts.mdx b/mintlify/ramps/accounts/external-accounts.mdx index a64ade151..f73e9f101 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` in a + region that applies the EU Travel Rule starts at + `PENDING_OWNERSHIP_VERIFICATION`. 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..aef96b105 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 applies the EU Travel Rule. 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: From d07ad7662fbef7444ced33077e47b3651fb69c20 Mon Sep 17 00:00:00 2001 From: shreyav Date: Fri, 4 Sep 2026 14:01:23 -0700 Subject: [PATCH 2/3] docs: describe ownership verification without tying it to one region Ownership verification is likely to apply outside the EU, so the status notes now state the rule generally and name the EU as an example, matching how the spec already phrases region-conditional requirements ("Required in regions that verify the email address before identity verification (e.g. the EU)"). The earlier wording made the EU Travel Rule the defining condition. Also adds the two statuses to the account model page, which carries a third copy of the external account status list. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NrRvhfq3vabYreWe8yvFD1 --- mintlify/platform-overview/core-concepts/account-model.mdx | 4 ++++ mintlify/ramps/accounts/external-accounts.mdx | 6 +++--- mintlify/snippets/external-accounts.mdx | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mintlify/platform-overview/core-concepts/account-model.mdx b/mintlify/platform-overview/core-concepts/account-model.mdx index c6559d1e1..2f926b68b 100644 --- a/mintlify/platform-overview/core-concepts/account-model.mdx +++ b/mintlify/platform-overview/core-concepts/account-model.mdx @@ -209,9 +209,13 @@ 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 +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). Transfers below the regulatory threshold go through while it is pending, so the status is not a block. Clear it by starting a challenge on the account and completing it with either a wallet signature or a liveness check. + 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). ### Using External Accounts diff --git a/mintlify/ramps/accounts/external-accounts.mdx b/mintlify/ramps/accounts/external-accounts.mdx index f73e9f101..a142e04f4 100644 --- a/mintlify/ramps/accounts/external-accounts.mdx +++ b/mintlify/ramps/accounts/external-accounts.mdx @@ -187,9 +187,9 @@ External accounts move through verification states: - A self-custody wallet registered with `ownershipType` set to `FIRST_PARTY` in a - region that applies the EU Travel Rule starts at - `PENDING_OWNERSHIP_VERIFICATION`. It is not blocked: conversions below the + 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 diff --git a/mintlify/snippets/external-accounts.mdx b/mintlify/snippets/external-accounts.mdx index aef96b105..946b97af8 100644 --- a/mintlify/snippets/external-accounts.mdx +++ b/mintlify/snippets/external-accounts.mdx @@ -1981,7 +1981,7 @@ Beneficiary data may be reviewed for risk and compliance. Only `ACTIVE` accounts | `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 applies the EU Travel Rule. 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. +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 From 11267bc53fd45144c733fb13ae993f71a0056065 Mon Sep 17 00:00:00 2001 From: shreyav Date: Fri, 4 Sep 2026 14:20:44 -0700 Subject: [PATCH 3/3] docs: explain how a self-custody wallet proves ownership The feature shipped with four endpoints, two verification methods, and no prose. `ownershipType`, `PENDING_OWNERSHIP_VERIFICATION` and `vaspName` appeared nowhere in the docs outside the changelog and the generated spec. Adds an Ownership Verification section to the account model page, next to the external account statuses it produces, following how Strong Customer Authentication is documented on the quote it attaches to rather than as a page of its own. Covers both methods, and the three things that are easy to get wrong: the signed message is matched character for character and carries a UTC date good for yesterday, today and tomorrow only; re-requesting a challenge returns the outstanding one rather than a fresh message; and `vaspName` belongs inside `accountInfo`, where at the top level it is ignored and a custodied wallet is treated as self-custody. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NrRvhfq3vabYreWe8yvFD1 --- .../core-concepts/account-model.mdx | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/mintlify/platform-overview/core-concepts/account-model.mdx b/mintlify/platform-overview/core-concepts/account-model.mdx index 2f926b68b..242069a89 100644 --- a/mintlify/platform-overview/core-concepts/account-model.mdx +++ b/mintlify/platform-overview/core-concepts/account-model.mdx @@ -214,10 +214,58 @@ PENDING → ACTIVE - **INACTIVE**: Disabled (can be reactivated) - **UNDER_REVIEW**: Additional review required -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). Transfers below the regulatory threshold go through while it is pending, so the status is not a block. Clear it by starting a challenge on the account and completing it with either a wallet signature or a liveness check. - 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