Skip to main content
The Authorize flow uses a quote to run card authorizations with 3DS. The widget creates the transaction, handles required bank redirects, and polls until a terminal status.

Prerequisites

  • A quote ID for the intended transaction (built from the user’s selected origin/destination).
  • The user is onboarded and verified (KYC is complete).
  • The necessary capability for the transaction is enabled for the user (e.g.: card deposit or card withdrawal)
  • The external account (card) is valid and allowed for authorization.

Flow overview

The authorization_event callback in the diagram maps to the Widget complete event and returns { transaction, trigger }.

What the flow covers

  • Create an Authorize session using an existing quote.
  • Run 3DS/bank authentication via the Widget and create the transaction.
  • Receive terminal status or keep monitoring if retries are exhausted.

Integration

Here’s how to run an authorization with the Payment Widget SDK.

Create Authorize session

You need a quote first (often produced after Select for Deposit/Withdrawal). Then create the payment session for flow: "authorize".

Create a quote

curl -X POST https://api.enterprise.uphold.com/transactions/quote \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -H "X-On-Behalf-Of: user <USER_ID>" \
  -d '{
    "denomination": {
      "amount": "50.00",
      "asset": "GBP",
      "target": "origin"
    },
    "destination": {
      "type": "account",
      "id": "79f52400-5c36-4ea0-94a8-00f406e0d905"
    },
    "origin": {
      "type": "external-account",
      "id": "7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7"
    }
  }'
const createQuote = async (transactionData) => {
  try {
    const quoteResponse = await fetch('https://api.enterprise.uphold.com/transactions/quote', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer <API_TOKEN>',
        'Content-Type': 'application/json',
        'X-On-Behalf-Of': 'user <USER_ID>'
      },
      body: JSON.stringify(transactionData)
    });

    if (!quoteResponse.ok) {
      throw new Error('Failed to create quote');
    }

    const quote = await quoteResponse.json();
    return quote;
  } catch (error) {
    console.error('Failed to create quote:', error);
    throw error;
  }
};

Create Authorize session

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": "authorize",
    "data": {
      "quoteId": "<QUOTE_ID>"
    }
  }'
const createAuthorizeSession = async (transactionData) => {
  // First, create a quote
  const quote = await createQuote(transactionData);

  // Then, create the authorize session using the quote id
  const sessionResponse = 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: 'authorize',
      data: { quoteId: quote.id }
    })
  });

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

  return await sessionResponse.json();
};

Set up the Widget

For native apps using a WebView, bridge events to the native side as noted in Installation & Setup.
import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk';

const initializeAuthorizeWidget = async (transactionData) => {
  const session = await createAuthorizeSession(transactionData);

  const widget = new PaymentWidget<'authorize'>(session.session, { debug: true });

  setupEventHandlers(widget);

  widget.mountIframe(document.getElementById('payment-container'));
};

Handle the complete event

The complete event does not guarantee success. Always check transaction.status and trigger.reason.
widget.on('complete', (event) => {
  const { transaction, trigger } = event.detail.value;
  handleAuthorizeComplete(transaction, trigger);
  widget.unmount();
});

Understand trigger reasons

const handleAuthorizeComplete = (transaction, trigger) => {
  if (trigger.reason === 'transaction-status-changed') {
    handleTransactionStatusChange(transaction);
  } else if (trigger.reason === 'max-retries-reached') {
    handleProcessingTransaction(transaction);
  }
};

const handleTransactionStatusChange = (transaction) => {
  switch (transaction.status) {
    case 'completed':
      handleTransactionSuccess(transaction);
      break;
    case 'failed':
      handleTransactionFailure(transaction);
      break;
    default:
      break;
  }
};

const handleTransactionFailure = (transaction) => {
  switch (transaction.statusDetails.reason) {
    case 'card-declined-by-bank':
    case 'card-expired':
    case 'card-permanently-declined-by-bank':
    case 'card-unauthorized':
    case 'card-unsupported':
    case 'insufficient-funds':
    case 'provider-maximum-limit-exceeded':
    case 'velocity':
    case 'unspecified-error':
    default:
      // Map to user-facing error messages
      break;
  }
};

const handleProcessingTransaction = (transaction) => {
  // Widget stopped polling; continue monitoring via webhooks or polling Get Transaction
  showProcessingMessage('Your transaction is being processed. Please check again later.');
};

Handle the cancel event

widget.on('cancel', () => {
  handleAuthorizeCancelled();
  widget.unmount();
});

const handleAuthorizeCancelled = () => {
  // Return user to your payment screen
};

Handle the error event

widget.on('error', (event) => {
  handleAuthorizeError(event.detail.error);
  widget.unmount();
});

const handleAuthorizeError = (error) => {
  const { code, details } = error;

  switch (code) {
    case 'entity_not_found':
      // Quote expired
      break;
    case 'insufficient_balance':
      break;
    case 'operation_not_allowed':
      // e.g., duplicate-withdrawal, card-unauthorized, unspecified-error
      break;
    case 'user_capability_failure':
      break;
    default:
      break;
  }
};

Complete implementation example

import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk';

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

  async authorize(transactionData) {
    try {
      const session = await this.createAuthorizeSession(transactionData);
      this.widget = new PaymentWidget<'authorize'>(session.session, { debug: true });
      this.setupEventHandlers();
      this.widget.mountIframe(document.getElementById('payment-container'));
    } catch (error) {
      this.handleInitializationError(error);
    }
  }

  async createAuthorizeSession(transactionData) {
    const quote = await createQuote(transactionData);

    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: 'authorize',
        data: { quoteId: quote.id }
      })
    });

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

    return await response.json();
  }

  setupEventHandlers() {
    this.widget.on('complete', (event) => {
      const { transaction, trigger } = event.detail.value;
      this.handleAuthorizeComplete(transaction, trigger);
      this.widget.unmount();
    });

    this.widget.on('cancel', () => {
      this.handleCancellation();
      this.widget.unmount();
    });

    this.widget.on('error', (event) => {
      this.handleError(event.detail.error);
      this.widget.unmount();
    });
  }

  handleAuthorizeComplete(transaction, trigger) {
    if (trigger.reason === 'transaction-status-changed') {
      this.handleTransactionStatusChange(transaction);
    } else if (trigger.reason === 'max-retries-reached') {
      this.handleProcessingTransaction(transaction);
    }
  }

  handleTransactionStatusChange(transaction) {
    if (transaction.status === 'completed') {
      // Success path
      return;
    }

    if (transaction.status === 'failed') {
      this.handleTransactionFailure(transaction);
    }
  }

  handleTransactionFailure(transaction) {
    // Map statusDetails.reason to user-friendly messaging
  }

  handleProcessingTransaction(transaction) {
    // Keep monitoring via webhooks or polling Get Transaction
  }

  handleCancellation() {
    // User exited the flow
  }

  handleError(error) {
    // Show user-friendly error
  }

  handleInitializationError(error) {
    // Show init error
  }
}

// Example transaction data
const transactionData = {
  denomination: {
    amount: '100.00',
    asset: 'USD',
    target: 'origin'
  },
  destination: {
    type: 'account',
    id: '79f52400-5c36-4ea0-94a8-00f406e0d905'
  },
  origin: {
    type: 'external-account',
    id: '7d5928c5-8ac4-4b0d-8b45-f332ba6a9de7'
  }
};

const authorizer = new TransactionAuthorizer();
authorizer.authorize(transactionData);

Additional resources