> ## 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 Travel Rule Widget

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

This guide walks you through handling a Travel Rule requirement when creating a crypto withdrawal using the Travel Rule Widget — from detecting the requirement on a quote to letting the widget collect and submit proof.

<Info>Want to collect and submit proof yourself instead of using a managed UI? See [Travel Rule withdrawal via the REST API](/developer-guides/travel-rule/withdrawal/via-api) — it also documents the RFI's `context` and `proofOptions` fields in detail.</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 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
  Usr->>U: Request to resolve RFI
  U->>B: Create widget session
  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/{requestForInformationId}
  A-->>W: { requestForInformation }
  W-->>U: complete
  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"
  }
}
```

## Resolve the Travel Rule RFI

The Travel Rule Widget allows the user to resolve a Travel Rule RFI 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 `referenceId` — the quote's id. Each session is single-use and bound to a specific quote.

```http theme={null}
POST /widgets/travel-rule/sessions
{
  "flow": "withdrawal-form",
  "data": {
    "referenceId": "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) => {
    // Widget has completed successfully — unmount the widget and proceed to create the transaction
    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': {
            // Widget has completed successfully — proceed to create the transaction
            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 proof — you don't need to forward any data yourself.

<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/overview#proof-types) used:

* **Synchronous proofs** (self-declaration, message signing) — the RFI is fully resolved as soon as `complete` fires.
* **Asynchronous proofs** (Satoshi test, a small on-chain test transaction) — `complete` firing does not mean the RFI is resolved yet; resolution depends on the microtransfer settling on-chain and confirmed by Uphold.

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

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. Once the RFI is resolved (`status: "ok"`), create a new quote if the original one has expired, and then proceed to [create the transaction](#create-the-transaction). If the RFI resolution fails (`status: "failed"`) or expires, retry the process with a new quote and resolve the Travel Rule requirement again.

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

## Create the transaction

Once the RFI is resolved (`status: "ok"`), create the transaction using the quote ID. If the original quote expired 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. 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/requests-for-information/get-request-for-information) that the RFI status is `ok`.
3. Call [Create transaction](/rest-apis/core-api/transactions/create-transaction) with the `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 equivalent deposit guide, see [Travel Rule deposit via the Travel Rule Widget](/developer-guides/travel-rule/deposit/via-widget).
