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

> Step-by-step guide to resolving a Travel Rule request for information on an on-hold crypto deposit.

This guide walks you through resolving a Travel Rule request for information on an on-hold crypto deposit — from detecting the on-hold status to resolving the RFI and allowing the transaction to proceed.

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

  A-->>B: webhook: transaction.status-changed (on-hold)
  B-->>Usr: Notify the user (deposit on hold)
  B->>A: GET /core/requests-for-information?referenceId={transactionId}
  A-->>B: { requestsForInformation }
  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 { travelRule }
  A-->>B: webhook: transaction.status-changed (completed/failed)
  B-->>Usr: Notify the user (outcome)
```

## Detect the on-hold transaction

When a crypto deposit is placed on hold due to a Travel Rule requirement, Uphold sends a `core.transaction.status-changed` webhook with `status: on-hold` and `statusDetails.reason: pending-requests-for-information`.

```json theme={null}
{
  "id": "<eventId>",
  "type": "core.transaction.status-changed",
  "createdAt": "2024-07-24T15:22:39Z",
  "data": {
    "transaction": {
      "id": "b97c7f64-1c34-4b6e-a8f2-3d5c4e9a1b72",
      "status": "on-hold",
      "statusDetails": {
        "reason": "pending-requests-for-information"
      }
    }
  }
}
```

<Note>Abbreviated — the transaction object includes additional fields.</Note>

<Info>If you are using polling instead of webhooks, check for `status: on-hold` and `statusDetails.reason: pending-requests-for-information` on the transaction object.</Info>

## Notify the user

Surface the on-hold status to the user out-of-band (email or push notification) — deposits can sit on-hold indefinitely until resolved.

## List transaction RFIs

Call [List request for information](/rest-apis/core-api/requests-for-information/list-requests-for-information) endpoint to retrieve all RFIs by `referenceId` (transaction or quote ID), then filter the results to keep only entries where type is "travel-rule". From that filtered set, check whether any RFI has a status of "pending" — if so, the transaction is still awaiting resolution. The deprecated [transaction-nested endpoint](/rest-apis/core-api/transactions/rfis/list-requests-for-information) still works but should not be used for new integrations.

```http theme={null}
GET /core/requests-for-information?referenceId={transactionId}
```

```json theme={null}
{
  "requestsForInformation": [
    {
      "id": "3f6d0c1e-a1bf-4b25-9802-2a3ee492d3c8",
      "type": "travel-rule",
      "status": "pending",
      "data": {},
      "createdAt": "2024-07-24T15:22:39Z",
      "updatedAt": "2024-07-24T15:22:39Z"
    }
  ]
}
```

<Info> If all travel-rule RFIs have a status of "ok", the transaction may have already been moved out of on-hold status automatically. Make sure to re-fetch the transaction and check its current status before taking further action. </Info>

## Resolve the RFI

The Travel Rule Widget allows the user to resolve a travel rule RFI for a specific transaction.

### Create a widget session

Create a session tied to the RFI by calling [Create session](/rest-apis/widgets-api/travel-rule/create-session) with `flow: deposit-form` and the `data` property containing the `requestForInformationId`. Each session is single-use and bound to a specific RFI.

```http theme={null}
POST /widgets/travel-rule/sessions
{
  "flow": "deposit-form",
  "data": {
    "requestForInformationId": "3f6d0c1e-a1bf-4b25-9802-2a3ee492d3c8"
  }
}
```

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

```json [expandable] theme={null}
{
  "session": {
    "flow": "deposit-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.05,
          "asset": "BTC",
          "source": ["bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"],
          "customer": {
            "name": "John Doe",
            "type": "natural"
          }
        }
      }
    }
  }
}
```

<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.</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<'deposit-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 listen for the webhook to confirm the transaction status change
    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 — unmount the widget, confirm the RFI is resolved, and listen for the webhook to confirm the transaction status change
            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 the 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. Proceed directly to [Monitoring for settlement](#monitoring-for-settlement) to listen for transaction status changes.
* **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 proceed to [Monitoring for settlement](#monitoring-for-settlement) to listen for transaction status changes.

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.

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('complete', () => {
    widget.unmount();
    // Monitor the RFI status via webhook or polling, then monitor the transaction status to confirm the transaction is no longer on-hold
  });
  ```

  ```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 monitor the transaction status to confirm the transaction is no longer on-hold
    break;
  ```
</CodeGroup>

### Handle cancellations

The `cancel` event fires when the user closes the widget without completing the form. The transaction remains `on-hold` until the RFI is resolved — create a new widget session for the same RFI 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>

## Monitoring for settlement

After the RFI is resolved, the transaction will move from `on-hold` to `processing` and then to either `completed` or `failed`.

Monitor the transaction status using webhooks (recommended) or polling (fallback):

* Webhook events (recommended):
  * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed)
    * `status: processing` → transaction is being processed
    * `status: completed` → necessary confirmations reached
    * `status: failed` → transaction failed
* Polling (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction)

When the transaction reaches `completed` or `failed`, notify the user of the outcome.

## Testing

To trigger a Travel Rule RFI on a deposit, use a GB user account and send 30 XRP from an unhosted (self-custodial) wallet to the user's Uphold deposit address.

Verify the following:

1. A `core.transaction.status-changed` webhook is received with `status: on-hold` and `statusDetails.reason: pending-requests-for-information`.
2. [List requests for information](/rest-apis/core-api/requests-for-information/list-requests-for-information) returns an RFI with `type: travel-rule` and `status: pending`.
3. After completing the widget flow, the RFI status changes to `ok` and the transaction moves back to `processing`.
4. The transaction status updates from `processing` to `completed` within a few minutes, assuming no other blockers.

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