> ## 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->>A: PUT /core/requests-for-information/{rfiId}
  A-->>W: { requestForInformation }
  W-->>U: complete { 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 `data` property containing 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"
        }
      },
      "requestForInformation": {
        "id": "a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
        "quoteId": "623000c8-9bdf-4a2b-aa3d-6a6b44a7f6a0"
      }
    }
  }
}
```

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

<CodeGroup>
  ```javascript Web SDK [expandable] 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'));
  ```

  ```html JavaScript [expandable] theme={null}
  <div id="travel-rule-container"></div>

  <script>
    // session is `response.session` from your backend
    async function initializeTravelRuleWidget(session) {
      const container = document.getElementById('travel-rule-container');
      const sessionOrigin = new URL(session.url).origin;

      const iframe = document.createElement('iframe');
      iframe.src = session.url;
      iframe.setAttribute('allow', "clipboard-write 'src'; clipboard-read 'src';");
      iframe.style.width = '100%';
      iframe.style.height = '100%';
      iframe.style.border = 'none';

      function teardown() {
        window.removeEventListener('message', onMessage);
        iframe.remove();
      }

      function onMessage(event) {
        if (event.origin !== sessionOrigin) return;

        switch (event.data?.type) {
          case 'load':
            iframe.contentWindow.postMessage({ ...session, options: {}, type: 'init' }, sessionOrigin);
            break;
          case 'ready':
            // Widget has loaded and is ready for user interaction
            break;
          case 'complete': {
            // `event.data.value` is the entire Travel Rule payload — forward it unchanged
            const { value: travelRule } = event.data;
            sendToBackend({ travelRule });
            teardown();
            break;
          }
          case 'cancel':
            teardown();
            break;
          case 'error':
            console.error('Travel Rule widget error:', event.data.error);
            teardown();
            break;
        }
      }

      window.addEventListener('message', onMessage);
      container.appendChild(iframe);
    }
  </script>
  ```
</CodeGroup>

<Info>
  Both examples above are for web applications — either creating the iframe yourself or letting the SDK do it. For native apps using a WebView, see [Native apps with the SDK](/widgets/travel-rule/installation-and-setup#native-apps-with-the-sdk) for the SDK's native-bundling pattern, or [Setup with JavaScript](/widgets/travel-rule/installation-and-setup#setup-with-javascript) for the no-SDK approach that loads the session `url` directly as the WebView's top-level page.
</Info>

### Handle complete event

Once the user finishes the form, the widget automatically resolves the RFI by calling [Update request for information](/rest-apis/core-api/requests-for-information/update-request-for-information) internally with the collected data.

<Note>
  Calling [Update request for information](/rest-apis/core-api/requests-for-information/update-request-for-information) yourself is only needed if you're not using the widget and are resolving the Travel Rule requirement directly — not part of this widget-based flow.
</Note>

When `complete` fires, unmount the widget and confirm the RFI is resolved before proceeding — whether it is immediately `ok` or still pending depends on the [proof type](/developer-guides/travel-rule/proof-types) used:

* **Synchronous proofs** (self-declaration, cryptographic signature) — the RFI is fully resolved as soon as `complete` fires. If the quote has expired, create a new quote and proceed directly to [Create transaction](/rest-apis/core-api/transactions/create-transaction).
* **Asynchronous proofs** (micro-transfer, e.g. a small on-chain test transaction) — `complete` firing does not mean the RFI is resolved yet; resolution depends on the micro-transfer settling on-chain and confirmed by Uphold. Wait for it to settle, then create a new quote before creating the transaction.

Use [Get request for information](/rest-apis/core-api/requests-for-information/get-request-for-information) to check which proof type was used and confirm the RFI's status.

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

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('complete', () => {
    widget.unmount();
    // Monitor the RFI status via webhook or polling, then create a new quote if expired and create the transaction
  });
  ```

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'complete':
    teardown();
    // Monitor the RFI status via webhook or polling, then create a new quote if expired and create the transaction
    break;
  ```
</CodeGroup>

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

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

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'cancel':
    teardown();
    // Redirect back or show a cancellation message
    break;
  ```
</CodeGroup>

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

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

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'error':
    console.error('Widget error:', event.data.error);
    teardown();
    // Show a user-friendly error message
    break;
  ```
</CodeGroup>

## 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. 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 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), complete the form — verify via [Get request for information](/rest-apis/core-api/transactions/rfis/get-request-for-information) that the RFI status is `ok`.
3. Create a new quote with the same parameters and call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the new `quoteId`; the transaction is created successfully.
4. A `core.transaction.status-changed` webhook is received with `status: completed` (or `failed` if the counterparty rejects the data).
5. 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).
