> ## Documentation Index
> Fetch the complete documentation index at: https://developer.uphold.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Address verification via the REST API

> Step-by-step guide to resolving a declared- or verified-level Travel Rule verification for a saved crypto address, ahead of a deposit or withdrawal.

This guide walks you through setting up and resolving a `declared`- or `verified`-level Travel Rule verification for a saved crypto address — see [Address Book overview](/developer-guides/travel-rule/address-book/overview) for how verification levels work.

## Prerequisites

* The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled.
* The user already has a `crypto` external account — see [Create external account](/rest-apis/core-api/external-accounts/create-external-account).
* For the `verified` level, the user also needs the `crypto-deposits` capability.

## Declared level

```mermaid theme={null}
sequenceDiagram
  autonumber
  participant Usr as User
  participant U as Your App
  participant B as Your Backend
  participant A as Uphold

  Usr->>U: Verify a saved crypto address
  U->>B: Set up verification
  B->>A: PUT /core/external-accounts/{id}/verifications/declared
  A-->>B: { verification (status: pending, requestForInformation) }
  B-->>U: Surface the self-declaration questions
  Usr->>U: Answer the questions
  U->>B: Submit proof
  B->>A: PUT /core/requests-for-information/{requestForInformationId}
  A-->>B: { requestForInformation (status: ok) }
  B-->>U: Verification completed
```

### Set up the verification

Call [Set up crypto external account verification](/rest-apis/core-api/external-accounts/setup-external-account-verification) with `level: "declared"`.

```http theme={null}
PUT /core/external-accounts/d0686449-36c7-c62e-b81f-de844bbc79d4/verifications/declared
```

A `201` response means a new verification was initiated. Its `requestForInformation.proofOptions.selfDeclaration.schema` describes exactly which fields to collect from the user.

```json [expandable] theme={null}
{
  "verification": {
    "via": "request-for-information",
    "level": "declared",
    "status": "pending",
    "requestForInformation": {
      "id": "19ca55fb-fc52-4209-b765-e3dbdc327231",
      "type": "crypto-address-travel-rule",
      "status": "pending",
      "context": {
        "network": "xrp-ledger",
        "addresses": [
          "rN7n7otQDd6FczFgLdlqtyMVrn3WD6CJYy"
        ]
      },
      "proofOptions": {
        "selfDeclaration": {
          "schema": {
            "type": "object",
            "required": [
              "isVASP",
              "isSelfHostedWallet",
              "isSelfOwnedCryptoAddress"
            ],
            "properties": {
              "isVASP": { "type": "boolean" },
              "isSelfHostedWallet": { "type": "boolean" },
              "isSelfOwnedCryptoAddress": { "type": "boolean" }
            }
          }
        }
      }
    }
  }
}
```

<Info>
  Calling this endpoint again is safe: a new request for information is initiated (`201`) only when there is no `declared` verification yet or the last one `failed`. While it's `pending`, or once it's `completed`, the same verification is returned unchanged (`200`) — it's never redone or regressed.
</Info>

<Warning>
  If a stronger, `verified`-level verification is already `completed` for this address, this returns `409 operation_not_allowed` with `details.reasons` including `stronger-verification-already-completed` — there's nothing to declare, a cryptographic proof already covers it.
</Warning>

### Resolve the Travel Rule RFI

For a `crypto-address-travel-rule` RFI, `selfDeclaration` is the only mechanism listed under `proofOptions` — see [Address Book overview](/developer-guides/travel-rule/address-book/overview) for why.

#### Self-declaration

Present the fields from `proofOptions.selfDeclaration.schema` to the user, then submit their answers with [Update request for information](/rest-apis/core-api/requests-for-information/update-request-for-information).

```http theme={null}
PUT /core/requests-for-information/19ca55fb-fc52-4209-b765-e3dbdc327231
{
  "proof": {
    "isVASP": false,
    "isSelfHostedWallet": true,
    "isSelfOwnedCryptoAddress": true
  }
}
```

```json [expandable] theme={null}
{
  "requestForInformation": {
    "id": "19ca55fb-fc52-4209-b765-e3dbdc327231",
    "type": "crypto-address-travel-rule",
    "status": "ok",
    "context": {
      "network": "xrp-ledger",
      "addresses": [
        "rN7n7otQDd6FczFgLdlqtyMVrn3WD6CJYy"
      ]
    },
    "proofOptions": {
      "selfDeclaration": {
        "schema": {
          "type": "object",
          "required": [
            "isVASP",
            "isSelfHostedWallet",
            "isSelfOwnedCryptoAddress"
          ],
          "properties": {
            "isVASP": { "type": "boolean" },
            "isSelfHostedWallet": { "type": "boolean" },
            "isSelfOwnedCryptoAddress": { "type": "boolean" }
          }
        }
      }
    },
    "proof": {
      "isVASP": false,
      "isSelfHostedWallet": true,
      "isSelfOwnedCryptoAddress": true
    }
  }
}
```

<Note>
  When `isVASP` is `true`, also include a `vaspDid` identifying the VASP that owns the address. If it can't be resolved by the Travel Rule provider, the request returns `404 entity_not_found` (`details.entity: "vasp"`) — ask the user to double-check the VASP they selected.
</Note>

### Confirm the verification

Call [Get external account verification](/rest-apis/core-api/external-accounts/get-external-account-verification) to confirm the verification is `completed`.

```http theme={null}
GET /core/external-accounts/d0686449-36c7-c62e-b81f-de844bbc79d4/verifications/declared
```

```json theme={null}
{
  "verification": {
    "via": "request-for-information",
    "level": "declared",
    "status": "completed",
    "completedAt": "2024-06-01T00:00:00.000Z",
    "requestForInformation": { "...": "..." }
  }
}
```

<Check>The address now carries a completed `declared` verification, reflected in the account's compact `verifications` array on [Get external account](/rest-apis/core-api/external-accounts/get-external-account).</Check>

### Testing

Verify the following:

1. [Set up crypto external account verification](/rest-apis/core-api/external-accounts/setup-external-account-verification) returns `201` with a `pending` verification and a `crypto-address-travel-rule` RFI.
2. [Update request for information](/rest-apis/core-api/requests-for-information/update-request-for-information) with a valid self-declaration proof returns `200` with `status: "ok"`.
3. [Get external account verification](/rest-apis/core-api/external-accounts/get-external-account-verification) now returns `status: "completed"`.
4. Calling [Set up crypto external account verification](/rest-apis/core-api/external-accounts/setup-external-account-verification) again returns `200` with the same, unchanged verification.

## Verified level

After setup, the `requestForInformation.proofOptions` offers two independent proof options for a `verified`-level verification — the user (or your backend) resolves whichever is more convenient: signing a message, or sending an exact on-chain amount. Either one resolves the same verification.

```mermaid theme={null}
sequenceDiagram
  autonumber
  participant Usr as User
  participant U as Your App
  participant B as Your Backend
  participant A as Uphold
  participant Chain as Blockchain

  Usr->>U: Verify a saved crypto address
  U->>B: Set up verification
  B->>A: PUT /core/external-accounts/{id}/verifications/verified
  A-->>B: { verification (status: pending, requestForInformation with proofOptions) }
  alt Message signing
    B-->>U: Surface message to sign
    Usr->>U: Sign message with wallet
    U->>B: Submit signed proof
    B->>A: PUT /core/requests-for-information/{requestForInformationId}
    A-->>B: { requestForInformation (status: ok) }
  else Satoshi test
    B-->>U: Surface deposit address, asset, and amount
    Usr->>Chain: Send exact Satoshi test amount
    Chain-->>A: Deposit detected
    loop Until status is ok
      B->>A: GET /core/requests-for-information/{requestForInformationId}
      A-->>B: { requestForInformation (status) }
    end
  end
  B-->>U: Verification completed
```

### Set up the verification

Call [Set up crypto external account verification](/rest-apis/core-api/external-accounts/setup-external-account-verification) with `level: "verified"`.

```http theme={null}
PUT /core/external-accounts/d0686449-36c7-c62e-b81f-de844bbc79d4/verifications/verified
```

A `201` response means a new verification was initiated, returning both proof options.

```json [expandable] theme={null}
{
  "verification": {
    "via": "request-for-information",
    "level": "verified",
    "status": "pending",
    "requestForInformation": {
      "id": "ba6b76c1-30d8-4b0a-9720-9c9f6c8a5b1e",
      "type": "crypto-address-verification",
      "status": "pending",
      "context": {
        "network": "bitcoin",
        "addresses": [
          "mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV"
        ]
      },
      "proofOptions": {
        "messageSigning": {
          "message": "I certify that the blockchain address mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV belongs to e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a on 2024-07-25T10:00:00.000Z",
          "schema": {
            "type": "object",
            "required": ["proof"],
            "properties": {
              "proof": {
                "type": "object",
                "title": "CryptographicalProof",
                "required": ["address", "attestation", "proof", "type"],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": ["bip-137", "eip-191", "sr25519", "ed25519"]
                  }
                }
              }
            }
          }
        },
        "satoshiTest": {
          "address": "mvGGPUZUf5NyLL7yQ5nZ1CvJ9zZ2Rq3xYo",
          "asset": "BTC",
          "amount": "0.00001088",
          "amountSubunits": "1088",
          "decimals": 8
        }
      }
    }
  }
}
```

<Info>
  Calling this endpoint again is safe: a new request for information is initiated (`201`) only when there is no `verified` verification yet or the last one `failed` (e.g. it expired). While it's `pending`, or once it's `completed`, the same verification is returned unchanged (`200`) — it's never redone or regressed.
</Info>

<Note>
  Setting up and getting a `verified` verification both return `409 user_capability_failure` with `details.capability.code: "crypto-deposits"` when the user lacks that capability. The `declared` level isn't affected.
</Note>

<Note>
  To receive the Satoshi test, Uphold uses the user's default account for the external account's asset, creating one if there is none, and creates a crypto deposit address for the network if needed. A new account or deposit address may therefore appear in the user's accounts after the first `verified` set up.
</Note>

### Submit a proof

<Tabs>
  <Tab title="Message signing">
    Have the user sign `proofOptions.messageSigning.message` **verbatim** with their wallet, producing a proof that validates against the returned `proofOptions.messageSigning.schema`. Then submit it with [Update request for information](/rest-apis/core-api/requests-for-information/update-request-for-information).

    ```http theme={null}
    PUT /core/requests-for-information/ba6b76c1-30d8-4b0a-9720-9c9f6c8a5b1e
    {
      "proof": {
        "type": "bip-137",
        "address": "mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV",
        "attestation": "I certify that the blockchain address mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV belongs to e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a on 2024-07-25T10:00:00.000Z",
        "proof": "HzY8u1v6c2f3d4e5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d"
      }
    }
    ```

    The proof's `type` is the signing scheme used by the wallet — one of the values in `proofOptions.messageSigning.schema.properties.proof.properties.type.enum` (e.g. `bip-137`, `eip-191`).

    ```json [expandable] theme={null}
    {
      "requestForInformation": {
        "id": "ba6b76c1-30d8-4b0a-9720-9c9f6c8a5b1e",
        "type": "crypto-address-verification",
        "status": "ok",
        "context": {
          "network": "bitcoin",
          "addresses": [
            "mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV"
          ]
        },
        "proofOptions": {
          "messageSigning": {
            "message": "I certify that the blockchain address mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV belongs to e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a on 2024-07-25T10:00:00.000Z"
          },
          "satoshiTest": {
            "address": "mvGGPUZUf5NyLL7yQ5nZ1CvJ9zZ2Rq3xYo",
            "asset": "BTC",
            "amount": "0.00001088",
            "amountSubunits": "1088",
            "decimals": 8
          }
        },
        "proof": {
          "type": "bip-137",
          "address": "mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV",
          "attestation": "I certify that the blockchain address mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV belongs to e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a on 2024-07-25T10:00:00.000Z",
          "proof": "HzY8u1v6c2f3d4e5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d"
        }
      }
    }
    ```

    <Warning>
      If the signature doesn't validate, this returns `409 proof_invalid`. A proof that doesn't match `proofOptions.messageSigning.schema` (for example a `type` outside the allowed values or a missing field) is rejected earlier with `400 request_invalid`, and an expired request for information returns `409 request_for_information_expired`.
    </Warning>
  </Tab>

  <Tab title="Satoshi test">
    Send **exactly** `proofOptions.satoshiTest.amount` of `proofOptions.satoshiTest.asset` on-chain to `proofOptions.satoshiTest.address`, including `reference` in the transaction when one is present in the proof option. Resolution is automatic once Uphold detects the deposit — there's nothing to submit to the RFI for this proof option. Poll [Get request for information](/rest-apis/core-api/requests-for-information/get-request-for-information) with the RFI's `id` until its `status` is `ok`.

    <Warning>
      Don't submit a Satoshi test proof to [Update request for information](/rest-apis/core-api/requests-for-information/update-request-for-information): this endpoint only accepts a message signing `proof` for a `crypto-address-verification` RFI, and anything else is rejected with `400 request_invalid`. A Satoshi test is resolved automatically once the expected deposit is received.
    </Warning>
  </Tab>
</Tabs>

### Confirm the verification

Call [Get external account verification](/rest-apis/core-api/external-accounts/get-external-account-verification) to confirm the verification is `completed`. The normalized public proof shape is the same regardless of which option was used to resolve it.

```http theme={null}
GET /core/external-accounts/d0686449-36c7-c62e-b81f-de844bbc79d4/verifications/verified
```

```json theme={null}
{
  "verification": {
    "via": "request-for-information",
    "level": "verified",
    "status": "completed",
    "completedAt": "2024-07-25T10:05:00.000Z",
    "proof": {
      "type": "bip-137",
      "address": "mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV",
      "attestation": "I certify that the blockchain address mmLpwJCSnw3SEiN1un8GACvwx4f1ddB6SV belongs to e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a on 2024-07-25T10:00:00.000Z",
      "proof": "HzY8u1v6c2f3d4e5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d"
    }
  }
}
```

<Check>The address now carries a completed `verified` verification, reflected in the account's compact `verifications` array on [Get external account](/rest-apis/core-api/external-accounts/get-external-account).</Check>

### Testing

Verify the following:

1. [Set up crypto external account verification](/rest-apis/core-api/external-accounts/setup-external-account-verification) with `level: "verified"` returns `201` with a `pending` verification and a `crypto-address-verification` RFI exposing both `messageSigning` and `satoshiTest` proof options.
2. [Update request for information](/rest-apis/core-api/requests-for-information/update-request-for-information) with a valid message signing proof returns `200` with `status: "ok"`, and with an invalid signature returns `409 proof_invalid`.
3. Sending the exact Satoshi test amount makes [Get request for information](/rest-apis/core-api/requests-for-information/get-request-for-information) return `status: "ok"`, without needing to update the RFI directly.
4. [Get external account verification](/rest-apis/core-api/external-accounts/get-external-account-verification) now returns `status: "completed"` with the normalized proof.
5. Calling [Set up crypto external account verification](/rest-apis/core-api/external-accounts/setup-external-account-verification) again while `pending` or once `completed` returns `200` with the same, unchanged verification.
6. Without the `crypto-deposits` capability, set up and get for `verified` return `409 user_capability_failure`.
