Skip to main content

Prerequisites

  • The user is onboarded and verified (KYC).
  • The user capabilities allow card, bank or crypto withdrawals.
  • The user has at least one external account on file, or is allowed to link a new one or provide a crypto withdrawal address.
  • Your backend can create quotes/transactions and listen to webhooks for status changes.

The flow

The route_selected callback in the diagram maps to the Widget complete event and returns { via, selection }.
The Select for Withdrawal flow lets users pick or add an external account or provide a crypto address to withdraw funds. Refer to the Bank Withdrawal flow or Crypto Withdrawal flow for the backend steps needed to process outgoing funds.

Available payment methods

  • Credit/Debit Cards - Visa and Mastercard
  • Bank transfer - e.g., FPS (UK), SEPA (EU), ACH (US) depending on availability
  • Crypto transfer - User-provided crypto address

Integration

Here’s how you can leverage the Payment Widget SDK to offer easy withdrawals:

Create Select for Withdrawal session

Before initializing the Widget, create a payment session for the Select for Withdrawal flow using the REST API as shown below.
curl -X POST https://api.enterprise.uphold.com/widgets/payment/sessions \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -H "X-On-Behalf-Of: user <USER_ID>" \
  -d '{
    "flow": "select-for-withdrawal"
  }'

Setting up the Widget

The example code below is for web applications. For native apps using a WebView, you’ll still need a bridge for events, as outlined in Installation & Setup.
Once you have the session, initialize the Payment Widget for the Select for Withdrawal flow:
import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk';

const initializeWithdrawalWidget = async () => {
  // Create the session
  const sessionData = await createWithdrawalSession();

  // Initialize the Widget with the session
  const widget = new PaymentWidget<'select-for-withdrawal'>(sessionData.session, { debug: true });

  // Set up event handlers (see sections below)
  setupEventHandlers(widget);

  // Optional: listen for the Widget to be fully loaded
  widget.on('ready', () => {
    console.log('Payment Widget is ready');
  });

  // Mount the Widget in an iframe
  widget.mountIframe(document.getElementById('payment-container'));
};

Handling the complete event

The complete event is fired when the user successfully selects a withdrawal method.
widget.on('complete', (event) => {
  const result = event.detail.value;
  console.log('Withdrawal destination selected:', result);

  // result.via indicates the type of selection
  const { via, selection } = result;

  // Process the selected external account
  handleWithdrawalMethodSelected(via, selection);

  // Clean up the Widget
  widget.unmount();
});

Event data

The complete event payload contains two primary properties:
  • via - Specifies the type of withdrawal method selected:
    • external-account - Previously linked payment method (e.g., saved card)
    • crypto-network - Crypto withdrawal requiring a destination address
  • selection - Contains the method data structure
External Account Selection (via: "external-account") When an external account is selected, the selection property contains an external account object with the saved payment method details. Crypto Network Method Selection (via: "crypto-network") When a crypto network is selected, the selection property contains an object with the following structure:
  • network - The destination crypto network (e.g. bitcoin)
  • address - The destination crypto address (e.g. 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa)
  • reference - The destination address reference, such as a destination tag (if applicable)
Validate user-provided addresses before creating the withdrawal. See Validate Network Address.
The following example demonstrates proper handling of both selection types:
const handleWithdrawalMethodSelected = (via, selection) => {
  if (via === 'external-account') {
    // selection is an ExternalAccount
    console.log('Selected external account:', {
      id: selection.id,
      type: selection.type, // 'card' or 'bank'
      label: selection.label, // e.g., "#1111"
      network: selection.network, // e.g., "visa", "fps"
      // Additional properties for external accounts
    });

    // Proceed with existing external account
    proceedWithExternalAccount(selection);
  } else if (via === 'crypto-network') {
    // selection contains crypto network details
    console.log('Selected crypto network:', {
      network: selection.network, // e.g., 'bitcoin', 'ethereum', 'xrp-ledger'
      address: selection.address,
      reference: selection.reference // For XRP, XLM, etc.
    });

    // Proceed with crypto withdrawal
    proceedWithCryptoNetwork(selection);
  }
};

const proceedWithExternalAccount = (externalAccount) => {
  // For external accounts, proceed to amount selection screen
};

const proceedWithCryptoNetwork = (cryptoNetwork) => {
  console.log('Crypto withdrawal details:', {
    network: cryptoNetwork.network, // e.g., 'bitcoin', 'ethereum', 'xrp-ledger'
    address: cryptoNetwork.address,
    reference: cryptoNetwork.reference // Destination tag for XRP, memo for XLM, etc.
  });

  // Proceed to collect amount and create withdrawal transaction
};

Monitoring for the withdrawal transaction

After your application collects the withdrawal details and creates the transaction, the Payment Widget does not monitor for the completion of the withdrawal. Your application may optionally implement transaction monitoring to detect when the withdrawal has been processed and provide real-time user feedback. This can be achieved by polling the transactions endpoint to identify when the withdrawal transaction status changes. The following example demonstrates how to poll for withdrawal transaction status:
Monitor withdrawal transaction
const monitorWithdrawal = async (transactionId) => {
  const checkWithdrawalStatus = async () => {
    try {
      const response = await fetch(`https://api.enterprise.uphold.com/transactions/${transactionId}`, {
        headers: {
          'Authorization': 'Bearer <API_TOKEN>',
          'Content-Type': 'application/json',
          'X-On-Behalf-Of': 'user <USER_ID>'
        }
      });

      const transaction = await response.json();
      return transaction;
    } catch (error) {
      console.error('Error checking withdrawal status:', error);
      return;
    }
  };

  // Poll every 30 seconds for up to 10 minutes
  const pollInterval = 30000; // 30 seconds
  const maxAttempts = 20; // 10 minutes total
  let attempts = 0;

  const poll = setInterval(async () => {
    attempts++;
    const transaction = await checkWithdrawalStatus();

    if (!transaction) {
      return;
    }

    const isTerminalStatus = ['completed', 'failed', 'cancelled'].includes(transaction.status);

    if (isTerminalStatus || attempts >= maxAttempts) {
      clearInterval(poll);

      if (isTerminalStatus) {
        handleWithdrawalStatusChange(transaction);
        return;
      }

      if (attempts >= maxAttempts) {
        console.log('Polling timeout reached without terminal status');
        handleWithdrawalTimeout();
      }
    }
  }, pollInterval);
};

const handleWithdrawalStatusChange = (transaction) => {
  console.log('Withdrawal status changed:', transaction);

  // Check transaction status to determine outcome
  if (transaction.status === 'completed') {
    // Update UI to show successful withdrawal
    showWithdrawalSuccess(transaction);
  } else if (transaction.status === 'failed') {
    // Update UI to show failed withdrawal
    showWithdrawalFailed(transaction);
  } else if (transaction.status === 'cancelled') {
    // Update UI to show cancelled withdrawal
    showWithdrawalCancelled(transaction);
  }
};

const handleWithdrawalTimeout = () => {
  console.log('Withdrawal monitoring timed out');
  // Show message that withdrawal is taking longer than expected
  showWithdrawalPending();
};

Handling the cancel event

The cancel event is fired when the user closes the Widget without selecting an external account.
widget.on('cancel', () => {
  console.log('User cancelled withdrawal destination selection');

  // Handle cancellation
  handleWithdrawalCancelled();

  // Clean up the Widget
  widget.unmount();
});

const handleWithdrawalCancelled = () => {
  // Redirect back to the previous page or main withdrawal page
};

Handling the error event

The error event is fired when an error occurs during the external account selection process.
widget.on('error', (event) => {
  const error = event.detail.error;
  console.error('Withdrawal Widget error:', error);

  // Handle the error
  handleWithdrawalError(error);

  // Clean up the Widget
  widget.unmount();
});

const handleWithdrawalError = (error) => {
  console.error('Error details:', {
    code: error.code,
    message: error.message
  });

  // Show user-friendly error message like 'An error occurred. Please try again later.'
};
The Payment Widget does not offer user-facing error handling. It’s the responsibility of the host application to present an error message to the user and unmount the Widget.

Complete implementation example

Here’s a complete example combining all the event handlers:
import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk';

class WithdrawalDestinationSelector {
  constructor() {
    this.widget = null;
  }

  async initialize() {
    try {
      // Create session for withdrawal flow
      const sessionData = await this.createWithdrawalSession();

      // Initialize Widget
      this.widget = new PaymentWidget<'select-for-withdrawal'>(sessionData.session, { debug: true });

      // Set up all event handlers
      this.setupEventHandlers();

      // Mount the Widget in an iframe
      this.widget.mountIframe(document.getElementById('payment-container'));
    } catch (error) {
      console.error('Failed to initialize Withdrawal Widget:', error);
      this.handleInitializationError(error);
    }
  }

  async createWithdrawalSession() {
    const response = await fetch('https://api.enterprise.uphold.com/widgets/payment/sessions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer <API_TOKEN>',
        'Content-Type': 'application/json',
        'X-On-Behalf-Of': 'user <USER_ID>'
      },
      body: JSON.stringify({
        flow: 'select-for-withdrawal'
      })
    });

    if (!response.ok) {
      throw new Error('Failed to create withdrawal session');
    }

    return await response.json();
  }

  setupEventHandlers() {
    // Handle successful selection
    this.widget.on('complete', (event) => {
      const { via, selection } = event.detail.value;
      console.log('Withdrawal method selected:', { via, selection });

      this.handleWithdrawalMethodSelected(via, selection);
      this.widget.unmount();
    });

    // Handle cancellation
    this.widget.on('cancel', () => {
      console.log('Selection cancelled');
      this.handleCancellation();
      this.widget.unmount();
    });

    // Handle errors
    this.widget.on('error', (event) => {
      console.error('Widget error:', event.detail.error);
      this.handleError(event.detail.error);
      this.widget.unmount();
    });
  }

  handleWithdrawalMethodSelected(via, selection) {
    if (via === 'external-account') {
      // This is an ExternalAccount (saved card or bank account)
      console.log('External account selected:', selection);
    } else if (via === 'crypto-network') {
      // This contains crypto network details
      console.log('Crypto network selected:', selection);

      const { network, address, reference } = selection;
      console.log('Crypto withdrawal details:', { network, address, reference });
    }
  }

  handleCancellation() {
    // Show cancellation message
    alert('Withdrawal method selection was cancelled');
  }

  handleError(error) {
    alert('An error occurred. Please try again later.');
  }

  handleInitializationError(error) {
    console.error('Initialization failed:', error);
    alert('Failed to load withdrawal methods. Please try again later.');
  }
}

// Usage
const withdrawalSelector = new WithdrawalDestinationSelector();
withdrawalSelector.initialize();

Next steps

After the user selects a withdrawal method, the next steps depend on what type of method was chosen:

For external accounts (e.g., saved cards, bank accounts)

  1. Collect withdrawal details - Collect remaining instructions from the user (e.g., amount, origin account)
  2. Create and authorize transaction - Use the selected external account to create the actual withdrawal transaction using the Authorize flow

For crypto network withdrawals

  1. Collect withdrawal details - Collect remaining instructions from the user (e.g., amount)
  2. Create withdrawal transaction - Create the withdrawal transaction to the user-provided crypto address
  3. Backend flow reference - For crypto withdrawals, align with the Crypto Withdrawal flow

Additional resources