> ## 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.

# Travel Rule withdrawal via the REST API

> Step-by-step guide to handling a Travel Rule requirement during a crypto withdrawal directly via the REST API.

This guide walks you through handling a Travel Rule requirement when creating a crypto withdrawal directly through the API — from detecting the requirement on a quote to submitting proof and creating the transaction.

<Info>Prefer a managed UI? See [Travel Rule withdrawal via the Travel Rule Widget](/developer-guides/travel-rule/withdrawal/via-widget) — it handles the proof collection form for you.</Info>

## Prerequisites

* The user has [completed onboarding](/developer-guides/user-onboarding/overview) and has the required capabilities enabled.

## Walkthrough

```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: Initiate withdrawal
  U->>B: Request quote
  B->>A: POST /core/transactions/quote
  A-->>B: { quote (requirements: ["travel-rule"]) }
  B-->>U: Surface Travel Rule requirement to user
  Usr->>U: Request to resolve RFI
  U->>B: Fetch transaction RFIs
  B->>A: GET /core/requests-for-information?reference={quoteId}
  A-->>B: { requestsForInformation }
  B-->>U: Surface the proof options to the user
  Usr->>U: Provide the requested proof
  U->>B: Submit proof
  B->>A: PUT /core/requests-for-information/{requestForInformationId}
  A-->>B: { requestForInformation }
  B->>A: POST /core/transactions
  A-->>B: { transaction }
  B-->>U: { transaction }
  A-->>B: webhook: transaction.status-changed (completed/failed)
  B-->>Usr: Notify the user
```

## Detect the requirement

When a quote is returned, check the `requirements` array. If it contains `travel-rule`, the requirement must be resolved before the transaction can be created. If `requirements` is empty, proceed directly to creating the transaction.

```json theme={null}
{
  "quote": {
    "id": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0",
    "requirements": [
      "travel-rule"
    ],
    "expiresAt": "2024-07-24T15:22:39Z"
  }
}
```

## List quote RFIs

Call [List request for information](/rest-apis/core-api/requests-for-information/list-requests-for-information) with `reference` set to the quote ID to find the pending `travel-rule` RFI.

```http theme={null}
GET /core/requests-for-information?reference=623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0
```

```json [expandable] theme={null}
{
  "requestsForInformation": [
    {
      "id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
      "type": "travel-rule",
      "status": "pending",
      "expiresAt": "2024-07-24T15:22:39Z",
      "context": {
        "network": "bitcoin",
        "addresses": [
          "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
        ]
      },
      "proofOptions": {
        "selfDeclaration": {
          "schema": { "properties": { "...": "..." } }
        },
        "messageSigning": {
          "schema": { "properties": { "...": "..." } },
          "message": "I certify that the blockchain address tb1q5smjnk8zms7sqygscqlwgzuga9gz3ycclunpy9 belongs to e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a on 2024-07-24T15:00:00.000Z"
        },
        "satoshiTest": {
          "address": "r3K7jMADbWWvPe6NVyMcTkg8ytHUESafuQ",
          "asset": "XRP",
          "amount": "1.6160",
          "amountSubunits": "1616000",
          "decimals": 6,
          "reference": "953588349"
        }
      }
    }
  ]
}
```

## Resolve the Travel Rule RFI

The RFI's `proofOptions` field lists which proof mechanisms can resolve it — only the mechanisms that actually apply to this withdrawal are present (a `satoshiTest` option only appears for networks where a microtransfer is a viable proof). See [Proof types](/developer-guides/travel-rule/overview#proof-types) for what each mechanism means and when it's used.

For a `type: "travel-rule"` RFI on a withdrawal, `proof` is submitted as `beneficiaryProof` — you're proving who the external receiver is. Submit it with [Update request for information](/rest-apis/core-api/requests-for-information/update-request-for-information).

### Self-declaration

Submit the attestation plus the compliance details described by `proofOptions.selfDeclaration.schema`:

```http theme={null}
PUT /core/requests-for-information/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d
{
  "proof": {
    "beneficiaryProof": {
      "type": "self-declaration",
      "attestation": "The address belongs to the reported beneficiary"
    },
    "beneficiary": {
      "beneficiaryPersons": [
        { "naturalPerson": { "name": { "nameIdentifier": [{ "primaryIdentifier": "Jones", "secondaryIdentifier": "James", "nameIdentifierType": "LEGL" }] } } }
      ]
    },
    "transactionAsset": "XRP",
    "transactionAmount": "18753916",
    "originatorEqualsBeneficiary": true
  }
}
```

```json [expandable] theme={null}
{
  "requestForInformation": {
    "id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
    "type": "travel-rule",
    "status": "ok",
    "proof": {
      "beneficiaryProof": {
        "type": "self-declaration",
        "attestation": "The address belongs to the reported beneficiary"
      },
      "beneficiary": {
        "beneficiaryPersons": [{ "naturalPerson": { "name": { "...": "..." } } }]
      },
      "isNonCustodial": true,
      "transactionAsset": "XRP",
      "transactionAmount": "18753916",
      "originatorEqualsBeneficiary": true
    }
  }
}
```

### Message signing

Have the user sign `proofOptions.messageSigning.message` with their wallet, then submit the signature:

```http theme={null}
PUT /core/requests-for-information/a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d
{
  "proof": {
    "beneficiaryProof": {
      "type": "message-signing",
      "scheme": "bip-322",
      "address": "tb1q5smjnk8zms7sqygscqlwgzuga9gz3ycclunpy9",
      "attestation": "I certify that the blockchain address tb1q5smjnk8zms7sqygscqlwgzuga9gz3ycclunpy9 belongs to did:pkh:bip122:000000000019d6689c085ae165831e93:tb1q5smjnk8zms7sqygscqlwgzuga9gz3ycclunpy9 on Wed, 16 Sep 2026 17:02:20 GMT",
      "proof": "H0pu/6Zpqv/5TgPn9ZbT0pFWiLVVlToPmoRh2F5rncgafqw8CI7T3NwbU0GWXMB5Qm6PORLjW21Yor0NHc8ScEc="
    }
  }
}
```

Uphold verifies the signature and enriches the response with `did` and `status`:

```json [expandable] theme={null}
{
  "requestForInformation": {
    "id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
    "type": "travel-rule",
    "status": "ok",
    "proof": {
      "beneficiaryProof": {
        "type": "message-signing",
        "scheme": "bip-322",
        "address": "tb1q5smjnk8zms7sqygscqlwgzuga9gz3ycclunpy9",
        "attestation": "I certify that the blockchain address tb1q5smjnk8zms7sqygscqlwgzuga9gz3ycclunpy9 belongs to did:pkh:bip122:000000000019d6689c085ae165831e93:tb1q5smjnk8zms7sqygscqlwgzuga9gz3ycclunpy9 on Wed, 16 Sep 2026 17:02:20 GMT",
        "proof": "H0pu/6Zpqv/5TgPn9ZbT0pFWiLVVlToPmoRh2F5rncgafqw8CI7T3NwbU0GWXMB5Qm6PORLjW21Yor0NHc8ScEc=",
        "did": "did:pkh:bip122:000000000019d6689c085ae165831e93:tb1q5smjnk8zms7sqygscqlwgzuga9gz3ycclunpy9",
        "status": "verified"
      },
      "transactionAsset": { "caip19": "bip122:000000000019d6689c085ae165831e93/slip44:0" },
      "originatorVASPdid": "did:ethr:0x74888b7ce6d95b3258d75b92432a951bf009d166",
      "transactionAmount": "48022",
      "originatorEqualsBeneficiary": true
    }
  }
}
```

### Satoshi test

Nothing is submitted through this endpoint — resolution is deferred until Uphold detects and confirms the on-chain microtransfer to the address described in `proofOptions.satoshiTest`. Poll [Get request for information](/rest-apis/core-api/requests-for-information/get-request-for-information) to know when it clears.

Once confirmed, the RFI resolves with a flat proof — unlike the other two types, it's not wrapped in `beneficiaryProof`:

```json theme={null}
{
  "requestForInformation": {
    "id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
    "type": "travel-rule",
    "status": "ok",
    "proof": {
      "type": "satoshi-test",
      "proof": "57CDD07727281AFE1669508D15A4FBE14B65CD78D1B6FF49788CC964DA307CE2"
    }
  }
}
```

Use [Get request for information](/rest-apis/core-api/requests-for-information/get-request-for-information) to confirm the RFI's status before proceeding. If it fails (`status: "failed"`) or expires, resolve the Travel Rule requirement again.

## Create the transaction

Once the RFI is resolved (`status: "ok"`), create the transaction using the quote ID. If the original quote expired while during the RFI process, create a new quote and proceed — no additional Travel Rule data needs to be passed, as the resolved RFI is tied to the destination address rather than a specific quote.

```http theme={null}
POST /core/transactions
{
  "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0"
}
```

Unlike a deposit hold, a withdrawal transaction is created right away, so failures surface after creation — either when the Travel Rule payload is rejected at creation time, or when the counterparty VASP later rejects the data.

After the transaction is created, the counterparty VASP has a window to review the Travel Rule data. If the data is rejected, Uphold emits a `core.transaction.status-changed` webhook with `status: failed` and `statusDetails.reason: travel-rule-verification-failed`. Common causes are the transaction being created before the RFI was fully resolved, or the beneficiary VASP being unrecognized or invalid. Notify the user and ask them to retry with a different destination.

## Testing

To trigger a Travel Rule requirement on a withdrawal, use a GB user account and create an XRP withdrawal quote to an external address for 30 XRP.

Verify the following:

1. The quote response includes `"travel-rule"` in the `requirements` array.
2. [List requests for information](/rest-apis/core-api/requests-for-information/list-requests-for-information) returns an RFI with `type: travel-rule`, `status: pending`, and a populated `proofOptions`.
3. [Update request for information](/rest-apis/core-api/requests-for-information/update-request-for-information) with a valid proof returns `200` with `status: "ok"`.
4. [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the `quoteId` succeeds.
5. A `core.transaction.status-changed` webhook is received with `status: completed` (or `failed` if the counterparty rejects the data).

For the equivalent deposit guide, see [Travel Rule deposit via the REST API](/developer-guides/travel-rule/deposit/via-api).
