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

> Step-by-step guide to handling a Travel Rule requirement during a crypto withdrawal.

This guide walks you through handling a Travel Rule requirement when creating a crypto withdrawal — from detecting the requirement on a quote to submitting the collected data with the transaction.

## 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 W as Travel Rule Widget
  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
  B->>A: POST /widgets/travel-rule/sessions
  A-->>B: { session }
  B-->>U: { session }
  U->>W: Initialize widget
  W-->>U: ready
  Usr->>W: Complete form
  W-->>U: complete { travelRule }
  U->>B: { travelRule }
  B->>A: POST /core/transactions
  A-->>B: { transaction }
  B-->>U: { transaction }
  A-->>B: webhook: transaction.status-changed (completed/failed)
  B-->>Usr: Notify the user (outcome)
```

## 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"
  }
}
```

## Resolve the requirement

The Travel Rule Widget allows the user to resolve a travel rule requirement for a specific quote.

### Create a widget session

Create a session tied to the quote by calling [Create session](/rest-apis/widgets-api/travel-rule/create-session) with `flow: withdrawal-form` and the `quoteId`. Each session is single-use and bound to a specific quote.

```http theme={null}
POST /widgets/travel-rule/sessions
{
  "flow": "withdrawal-form",
  "data": {
    "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0"
  }
}
```

A successful response returns the session data needed to initialize the widget.

```json [expandable] theme={null}
{
  "session": {
    "flow": "withdrawal-form",
    "url": "https://travel-rule-widget.enterprise.uphold.com/",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "data": {
      "provider": "notabene",
      "parameters": {
        "init": {
          "authToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
          "nodeUrl": "https://api.notabene.id"
        },
        "options": {},
        "transaction": {
          "amountDecimal": 0.00121023,
          "asset": "BTC",
          "destination": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
        }
      }
    }
  }
}
```

<Note>Widget sessions expire after 2 minutes. If the session expires before the user opens the widget — or while they are mid-form — the widget emits an `error` event. Create a new session and re-mount to let the user retry. If the quote has also expired, create a new quote first before creating a new session.</Note>

### Set up the widget

Initialize the widget using the session returned from the API and mount it into your application. The widget does not unmount itself — always call `unmount()` after handling any event.

```javascript theme={null}
import { TravelRuleWidget } from '@uphold/enterprise-travel-rule-widget-web-sdk';

const widget = new TravelRuleWidget<'withdrawal-form'>(session, { debug: true });

widget.on('ready', () => {
  // Widget has loaded and is ready for user interaction
});

widget.on('complete', (event) => {
  // `event.detail.value` is the entire Travel Rule payload — forward it unchanged
  const { value: travelRule } = event.detail;
  sendToBackend({ travelRule });
  widget.unmount();
});

widget.on('cancel', () => {
  widget.unmount();
});

widget.on('error', (event) => {
  console.error('Travel Rule widget error:', event.detail.error);
  widget.unmount();
});

widget.mountIframe(document.getElementById('travel-rule-container'));
```

<Info>The example above is for web applications. For native apps using a WebView, see [Native app integration](/widgets/travel-rule/installation-and-setup#native-app-integration).</Info>

### Handle complete event

Once the widget emits `complete`, send the payload to your backend. The `params.travelRule` field is the complete `event.detail.value` object from the complete event, passed through unchanged — including its nested `value` field, which you do not unwrap. See [complete event](/widgets/travel-rule/sdk-reference#complete) for the event reference. For withdrawals the data is nested under `params.travelRule`, unlike deposits where it goes in the top-level `data` field of the RFI update.

```http theme={null}
POST /core/transactions
```

```json [expandable] theme={null}
{
  "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0",
  "params": {
    "travelRule": {
      "txUpdate": {
        "transactionAsset": { "caip19": "xrpl:0/slip44:144" },
        "transactionAmount": "30000000",
        "originatorEqualsBeneficiary": false,
        "beneficiaryVASPdid": "did:ethr:0x…",
        "originatorVASPdid": "did:ethr:0x…",
        "beneficiary": {},
        "originator": {
          "originatorPersons": [
            {
              "naturalPerson": {
                "name": {
                  "nameIdentifier": [
                    { "primaryIdentifier": "Doe", "secondaryIdentifier": "John", "nameIdentifierType": "LEGL" }
                  ]
                }
              }
            }
          ]
        }
      },
      "errors": [],
      "status": "pending",
      "valid": true,
      "value": {
        "amountDecimal": 30,
        "asset": "XRP",
        "customer": { "name": "John Doe", "type": "natural" },
        "source": ["rExampleXRPLAddress0000000000000"],
        "agent": {
          "type": "VASP",
          "did": "did:ethr:0x…",
          "name": "Example VASP",
          "verified": true
        },
        "counterparty": { "type": "natural", "name": "John Doe", "verified": false },
        "account": {
          "caip10": "xrpl:0:rExampleXRPLAddress0000000000000",
          "blockchainAddress": "rExampleXRPLAddress0000000000000",
          "chain": "xrpl:0",
          "did": "did:pkh:xrpl:0:rExampleXRPLAddress0000000000000",
          "valid": true
        }
      },
      "ivms101": {
        "originator": {
          "originatorPersons": [
            {
              "naturalPerson": {
                "name": {
                  "nameIdentifier": [
                    { "primaryIdentifier": "Doe", "secondaryIdentifier": "John", "nameIdentifierType": "LEGL" }
                  ]
                }
              }
            }
          ]
        }
      }
    }
  }
}
```

<Warning>
  The original quote may have expired while the user was completing the widget form. If so, create a new quote before proceeding — the Travel Rule data collected by the widget remains valid. Include the same `travelRule` object in the transaction request with the updated `quoteId`.
</Warning>

### Handle cancellations

The `cancel` event fires when the user closes the widget without completing the form. The quote is not affected — it remains valid until it expires, so a new widget session can be created for the same quote to let the user retry. See [cancel event](/widgets/travel-rule/sdk-reference#cancel) for the event reference.

```javascript theme={null}
widget.on('cancel', () => {
  widget.unmount();
  // Redirect back or show a cancellation message
});
```

### Handle errors

The `error` event fires when an unrecoverable error occurs. See [error event](/widgets/travel-rule/sdk-reference#error) for the full error shape and available properties.

```javascript theme={null}
widget.on('error', (event) => {
  console.error('Widget error:', event.detail.error);
  widget.unmount();
  // Show a user-friendly error message
});
```

## Transaction failures

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.

### Transaction creation errors

A transaction with an `unspecified-error` can indicate a rejected Travel Rule payload. The transaction was not completed. Collect fresh data via the widget and retry — the original `quoteId` remains valid unless it has since expired, in which case create a new quote first.

### Counterparty rejection

After the transaction is created, the counterparty VASP has a window to review the Travel Rule data. This surfaces in two ways:

* **Transaction fails** — Uphold emits a `core.transaction.status-changed` webhook with `status: failed` and `statusDetails.reason: travel-rule-verification-failed`. Notify the user; they may need to retry with a different destination.
* **New RFI created** — the counterparty requests additional information. An RFI of type `travel-rule` is attached to the transaction. List the pending RFIs via `GET /core/transactions/{id}/requests-for-information`, create a widget session with the `requestForInformationId`, and resolve via `PUT /core/transactions/{id}/requests-for-information/{rfiId}`. See the [deposit guide](/developer-guides/travel-rule/deposit#list-transaction-rfis) for the full flow.

## Testing

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

Verify the following:

1. The quote response includes `"travel-rule"` in the `requirements` array.
2. After [creating a widget session](/rest-apis/widgets-api/travel-rule/create-session), completing the widget flow, and calling [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the Travel Rule data, the transaction is created successfully.
3. A `core.transaction.status-changed` webhook is received with `status: completed` (or `failed` if the counterparty rejects the data).
4. The transaction status updates to `completed` within a few minutes, assuming no other blockers.

For the deposit flow, see [Travel Rule — deposit flow](/developer-guides/travel-rule/deposit).
