Skip to main content

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.

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

Walkthrough

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.
{
  "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"
      }
    }
  }
}
Abbreviated — the transaction object includes additional fields.
If you are using polling instead of webhooks, check for status: on-hold and statusDetails.reason: pending-requests-for-information on the transaction object.

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 requests for information endpoint to retrieve all RFIs associated with the transaction, 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.
GET /core/transactions/{transactionId}/requests-for-information
{
  "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"
    }
  ]
}
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.

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 with flow: deposit-form and the data: requestForInformationId. Each session is single-use and bound to a specific RFI.
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.
{
  "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"
          }
        }
      }
    }
  }
}
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.

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.
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) => {
  // `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'));
The example above is for web applications. For native apps using a WebView, see Native app integration.

Handle the complete event

Once the widget emits complete, send the payload to your backend. The request body’s data field is the full event.detail.value object from the complete event, passed through unchanged. See complete event for the event reference.
PUT /core/transactions/{transactionId}/requests-for-information/{requestForInformationId}
{
  "data": {
    "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" }
                ]
              }
            }
          }
        ]
      }
    }
  }
}
A successful response returns the updated RFI with status: ok. The transaction resumes processing automatically.
{
  "requestForInformation": {
    "id": "3f6d0c1e-a1bf-4b25-9802-2a3ee492d3c8",
    "type": "travel-rule",
    "status": "ok",
    "createdAt": "2024-07-24T15:22:39Z",
    "updatedAt": "2024-07-24T15:25:12Z",
    "data": {
      "originator": {
        "originatorPersons": [
          {
            "naturalPerson": {
              "name": {
                "nameIdentifier": [
                  {
                    "primaryIdentifier": "Doe",
                    "secondaryIdentifier": "John",
                    "nameIdentifierType": "LEGL"
                  }
                ]
              }
            }
          }
        ]
      },
      "beneficiary": {},
      "originatorProof": {
        "type": "self-declaration",
        "status": "verified",
        "confirmed": true
      },
      "transactionAsset": {
        "caip19": "xrpl:0/slip44:144"
      },
      "transactionAmount": "4000000",
      "originatorEqualsBeneficiary": true
    }
  }
}

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 for the event reference.
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 for the full error shape and available properties.
widget.on('error', (event) => {
  console.error('Widget error:', event.detail.error);
  widget.unmount();
  // Show a user-friendly error message
});

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 returns an RFI with type: travel-rule and status: pending.
  3. After completing the widget flow and calling Update request for information, 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.