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

Walkthrough

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.
{
  "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 with flow: withdrawal-form and the quoteId. Each session is single-use and bound to a specific quote.
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.
{
  "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"
        }
      }
    }
  }
}
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.

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

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 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.
POST /core/transactions
{
  "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" }
                  ]
                }
              }
            }
          ]
        }
      }
    }
  }
}
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.

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 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
});

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 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, completing the widget flow, and calling 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.