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

# PayPal withdrawal via the Payment Widget

> Pay out from a user's account to PayPal using the Uphold Payment Widget.

export const apmLabel_1 = "PayPal"

export const apmLabel_0 = "PayPal"

export const direction_0 = "withdrawal"

The Payment Widget handles PayPal selection for withdrawals via the **Select for Withdrawal flow**, where users can select or unlink a previously linked PayPal account directly in the widget. After the user confirms a PayPal withdrawal quote, the Payment Widget creates the transaction and completes the PayPal authorization — whether the account is new or already linked — via the **Authorize flow**.

## Prerequisites

* The user has [completed onboarding](/developer-guides/user-onboarding/overview), and a **verified phone number**.
* The `paypal-withdrawals` capability is enabled.
* A **funded account** to debit the funds from.
* The Payment Widget is set up in your frontend. See [Installation and setup](/widgets/payment/installation-and-setup).

## Walkthrough

The diagram shows a first-time authorization. For an already-authorized account, skip the sign-in — the widget only collects device data and reuses the stored authorization.

```mermaid theme={null}
sequenceDiagram
  autonumber
  participant Usr as User
  participant U as Your App
  participant B as Your Backend
  participant P as Payment Widget
  participant A as Uphold

  Usr->>U: Start withdrawal
  U->>B: List accounts
  B->>A: GET /core/accounts
  A-->>B: { accounts }
  B-->>U: { accounts }
  Usr->>U: Choose source account
  U->>B: Create widget session
  B->>A: Create widget session (select-for-withdrawal)
  A-->>B: { session }
  B-->>U: { session }
  U->>P: Initialize widget
  Usr->>P: Select PayPal
  P-->>U: complete { via, selection }
  Usr->>U: Choose amount
  U->>B: Create quote
  B->>A: Create quote (account → APM)
  A-->>B: { quote }
  B-->>U: { quote }
  U->>B: Create authorize session
  B->>A: Create widget session (authorize)
  A-->>B: { session }
  B-->>U: { session }
  U->>P: Initialize widget
  Usr->>P: Sign in & authorize (first time only)
  P->>A: Create transaction
  A-->>P: { transaction }
  P-->>U: complete { transaction, trigger }
  B-->>Usr: Notify the user
```

***

## Select source account

{apmLabel_1} withdrawals can be sourced from any account. If the selected account is not in the {apmLabel_1} account's currency, the balance will be converted at the time of the transaction using Uphold's prevailing rate. Make sure the origin asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals).

Call [List accounts](/rest-apis/core-api/accounts/list-accounts) to retrieve the user's accounts and let them pick the one to withdraw from.

```http theme={null}
GET /core/accounts
```

```json theme={null}
{
  "accounts": [
    {
      "id": "a00507fe-628c-4f27-ae81-e1c40b2a8fb8",
      "ownerId": "e4ce04dc-67b7-4e9f-af91-482cb6f9fc4a",
      "label": "My USD account",
      "asset": "USD",
      "balance": {
        "total": "500.00",
        "available": "500.00"
      }
    }
  ]
}
```

***

## Select withdrawal method

The Payment Widget's **Select for Withdrawal flow** presents the available payment methods, letting the user select PayPal when it's available. It performs **selection only**: it does not create the quote or the transaction.

### Create a widget session

Call [Create widget session](/rest-apis/widgets-api/payment/create-session) to start the `select-for-withdrawal` flow.

```http theme={null}
POST /widgets/payment/sessions
{
  "flow": "select-for-withdrawal"
}
```

```json theme={null}
{
  "session": {
    "flow": "select-for-withdrawal",
    "url": "https://payment.enterprise.uphold.com/",
    "token": "GEbRxBN...edjnXbL"
  }
}
```

Pass `response.session` to your frontend to initialize the widget.

### Set up the widget

<CodeGroup>
  ```javascript Web SDK [expandable] theme={null}
  import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk';

  const initializeWithdrawalWidget = async (session) => {
    const widget = new PaymentWidget<'select-for-withdrawal'>(session, { debug: true });

    widget.on('complete', (event) => {
      console.log('Complete', JSON.stringify(event.detail.value));
    });

    widget.on('cancel', () => {
      console.log('Cancelled');
      widget.unmount();
    });

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

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

  ```html JavaScript [expandable] theme={null}
  <div id="payment-container"></div>

  <script>
    // session is `response.session` from your backend
    async function initializeWithdrawalWidget(session) {
      const container = document.getElementById('payment-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':
            break;
          case 'complete':
            console.log('Complete', JSON.stringify(event.data.value));
            teardown();
            break;
          case 'cancel':
            console.log('Cancelled');
            teardown();
            break;
          case 'error':
            console.error('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/payment/installation-and-setup#native-apps-with-the-sdk) for the SDK's native-bundling pattern, or [Setup with JavaScript](/widgets/payment/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

The `complete` event fires after the user selects PayPal. The event payload includes the selected PayPal external account — if it has already been previously authorized.

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('complete', (event) => {
    const { via, selection } = event.detail.value;

    // If via is 'apm', you can check the selected method through the property `selection.method`, which will be 'paypal' in this case.
    if (via === 'apm' && selection.method === 'paypal') {
      handlePayPalSelected();
    }

    widget.unmount();
  });
  ```

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'complete': {
    const { via, selection } = event.data.value;

    // If via is 'apm', you can check the selected method through the property `selection.method`, which will be 'paypal' in this case.
    if (via === 'apm' && selection.method === 'paypal') {
      handlePayPalSelected();
    }

    teardown();
    break;
  }
  ```
</CodeGroup>

Once you have the selection, proceed to [Create a quote](#create-a-quote).

### Handle cancellations

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('cancel', () => {
    widget.unmount();
    // Return the user to the previous screen
  });
  ```

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'cancel':
    teardown();
    // Return the user to the previous screen
    break;
  ```
</CodeGroup>

### Handle errors

The `error` event fires for critical unrecoverable errors.

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

<Warning>The Payment Widget handles most errors internally. For unrecoverable errors, the widget fires an `error` event. It is the host application's responsibility to handle these events, present an error message to the user, and unmount the widget.</Warning>

***

## Create a quote

Call [Create quote](/rest-apis/core-api/transactions/create-quote) with the user's account as the origin and PayPal as the destination.

There are two ways to specify the PayPal destination:

* **`apm` shortcut** — use `type: "apm"` with `method: "paypal"`. This is always accepted, whether or not the user already has a linked PayPal account.
* **`external-account` reference** — if the user already has a linked PayPal account, you can reference it directly with `type: "external-account"` and its `id` (from [List external accounts](/rest-apis/core-api/external-accounts/list-external-accounts)):

  ```json theme={null}
  {
    "destination": {
      "type": "external-account",
      "id": "9d3cce5f-a448-f985-b64f-a62930b18eea"
    }
  }
  ```

The example below uses the `apm` shortcut.

```http theme={null}
POST /core/transactions/quote
{
  "origin": {
    "type": "account",
    "id": "b8618cd1-ccb0-4a72-985f-3bb0d268dabd"
  },
  "destination": {
    "type": "apm",
    "method": "paypal"
  },
  "denomination": {
    "asset": "USD",
    "amount": "15",
    "target": "origin"
  }
}
```

A successful response includes the quote details and a `requirements` array. If it contains `authorize:paypal`, the user must authorize PayPal before the transaction can be created.

```json [expandable] theme={null}
{
  "quote": {
    "id": "c3e8d2f7-9a41-4b75-b8e3-1d6f4a9c2e57",
    "origin": {
      "amount": "15.00",
      "asset": "USD",
      "node": {
        "type": "account",
        "id": "b8618cd1-ccb0-4a72-985f-3bb0d268dabd",
        "ownerId": "48e40cb2-6c34-44ce-b2f1-6adac459bb37"
      },
      "rate": "1"
    },
    "destination": {
      "amount": "14.00",
      "asset": "USD",
      "node": {
        "type": "apm",
        "method": "paypal"
      },
      "rate": "1"
    },
    "denomination": {
      "asset": "USD",
      "amount": "15.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [
      {
        "type": "withdrawal",
        "code": "alternative-payment-method-withdrawals",
        "asset": "USD",
        "amount": "1.00",
        "percentage": "1.75"
      }
    ],
    "expiresAt": "2025-06-18T01:55:39Z",
    "requirements": [
      "authorize:paypal"
    ]
  }
}
```

***

## Present the order summary

Before creating the transaction, display an order summary of the quote — the amount, fees, and the origin and destination — so the user can review it. Here's an example:

<Frame>
  <div style={{maxWidth: '400px', margin: '0 auto'}}>
    <img src="https://mintcdn.com/uphold-d4756e17/vIioqA6Ud5_sRLRt/developer-guides/apm-transfers/_media/paypal-withdrawal-transaction-preview.png?fit=max&auto=format&n=vIioqA6Ud5_sRLRt&q=85&s=945f0b4780a64043496a9f89ab0a5f4a" alt="PayPal order summary" width="1466" height="2920" data-path="developer-guides/apm-transfers/_media/paypal-withdrawal-transaction-preview.png" />
  </div>
</Frame>

***

## Authorize and create the transaction

After the user confirms the PayPal deposit quote, hand off to the Payment Widget **Authorize flow**. It creates the transaction, runs the PayPal authorization, and polls until a terminal status is reached — so the same flow works for both new and already-authorized accounts.

### Create an authorize session

Call [Create widget session](/rest-apis/widgets-api/payment/create-session) with `flow: "authorize"`, the `quoteId` and with the `requirements` array containing `authorize:paypal`.

```http theme={null}
POST /widgets/payment/sessions
{
  "flow": "authorize",
  "data": {
    "quoteId": "<quoteId>",
    "requirements": [
      "authorize:paypal"
    ]
  }
}
```

```json theme={null}
{
  "session": {
    "flow": "authorize",
    "url": "https://payment.enterprise.uphold.com/",
    "token": "GEbRxBN...edjnXbL",
    "data": {
      "quoteId": "<quoteId>",
      "requirements": [
        "authorize:paypal"
      ]
    }
  }
}
```

### Set up the widget

Initialize the widget with the session. The widget interacts with PayPal, creates the transaction, and polls until a terminal status is reached. For a **new account** the user signs in to PayPal to authorize; for an **already-authorized account** they do not sign in again — the widget only collects device data and reuses the stored authorization.

<CodeGroup>
  ```javascript Web SDK [expandable] theme={null}
  import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk';

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

    widget.on('complete', (event) => {
      const { transaction, trigger } = event.detail.value;
      console.log('Complete', transaction.status, trigger.reason);
      widget.unmount();
    });

    widget.on('cancel', () => {
      console.log('Cancelled');
      widget.unmount();
    });

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

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

  ```javascript JavaScript [expandable] theme={null}
  // session is `response.session` from your backend
  async function initializeAuthorizeWidget(session) {
    const container = document.getElementById('payment-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 'complete': {
          const { transaction, trigger } = event.data.value;
          console.log('Complete', transaction.status, trigger.reason);
          teardown();
          break;
        }
        case 'cancel':
          console.log('Cancelled');
          teardown();
          break;
        case 'error':
          console.error('Error', event.data.error);
          teardown();
          break;
      }
    }

    window.addEventListener('message', onMessage);
    container.appendChild(iframe);
  }
  ```
</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/payment/installation-and-setup#native-apps-with-the-sdk) for the SDK's native-bundling pattern, or [Setup with JavaScript](/widgets/payment/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

<Warning>The `complete` event does not guarantee success. Always check `transaction.status` and `trigger.reason`.</Warning>

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('complete', (event) => {
    const { transaction, trigger } = event.detail.value;

    if (trigger.reason === 'transaction-status-changed') {
      if (transaction.status === 'completed') {
        // Show success — the account is now authorized and the transfer settled
      } else if (transaction.status === 'failed') {
        // Map transaction.statusDetails.reason to a user-facing message
      }
    } else if (trigger.reason === 'max-retries-reached') {
      // Widget stopped polling — continue monitoring via webhooks or polling
    }

    widget.unmount();
  });
  ```

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'complete': {
    const { transaction, trigger } = event.data.value;

    if (trigger.reason === 'transaction-status-changed') {
      if (transaction.status === 'completed') {
        // Show success — the account is now authorized and the transfer settled
      } else if (transaction.status === 'failed') {
        // Map transaction.statusDetails.reason to a user-facing message
      }
    } else if (trigger.reason === 'max-retries-reached') {
      // Widget stopped polling — continue monitoring via webhooks or polling
    }

    teardown();
    break;
  }
  ```
</CodeGroup>

Failure reasons in `transaction.statusDetails.reason`:

| Reason                             | Description                                      |
| ---------------------------------- | ------------------------------------------------ |
| `apm-authorization-failed`         | The PayPal authorization could not be completed  |
| `apm-payment-method-declined`      | PayPal declined the payment method               |
| `apm-account-holder-data-mismatch` | The account holder details did not match         |
| `apm-missing-account-holder-data`  | Required account holder details were missing     |
| `insufficient-funds`               | The origin account has insufficient funds        |
| `provider-maximum-limit-exceeded`  | The transaction exceeds provider limits          |
| `velocity`                         | The transaction was blocked by velocity rules    |
| `unspecified-error`                | The transaction failed for an unspecified reason |

### Handle cancellations

The `cancel` event fires when the user navigates back without completing authorization.

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('cancel', () => {
    widget.unmount();
    // Return the user to the previous screen
  });
  ```

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'cancel':
    teardown();
    // Return the user to the previous screen
    break;
  ```
</CodeGroup>

### Handle errors

The `error` event fires for critical unrecoverable errors.

<CodeGroup>
  ```javascript Web SDK theme={null}
  widget.on('error', (event) => {
    const { code, details } = event.detail.error;
    console.error('Widget error:', code, details);
    widget.unmount();
    // Show a user-friendly error message
  });
  ```

  ```javascript JavaScript theme={null}
  // Inside the onMessage switch from Set up the widget
  case 'error': {
    const { code, details } = event.data.error;
    console.error('Widget error:', code, details);
    teardown();
    // Show a user-friendly error message
    break;
  }
  ```
</CodeGroup>

Error codes in `event.detail.error.code`:

| Code                      | Description                                               |
| ------------------------- | --------------------------------------------------------- |
| `entity_not_found`        | The quote was not found or has expired                    |
| `insufficient_balance`    | The origin has insufficient balance                       |
| `operation_not_allowed`   | The operation is not permitted                            |
| `user_capability_failure` | The user lacks the required capability for this operation |

<Warning>The Payment Widget handles most errors internally. For unrecoverable errors, the widget fires an `error` event. It is the host application's responsibility to handle these events, present an error message to the user, and unmount the widget.</Warning>

***

## Monitor for settlement

{apmLabel_0} {direction_0} transactions remain in `processing` while the payment settles. Monitor until the transaction reaches a terminal state.

* **Webhook events** (recommended):
  * [core.transaction.created](/rest-apis/core-api/transactions/webhooks/transaction-created) — `status: processing` → transaction created, pending settlement
  * [core.transaction.status-changed](/rest-apis/core-api/transactions/webhooks/transaction-status-changed) — `status: completed` → funds settled; `status: failed` → irrecoverable error
* **Polling** (fallback): [Get transaction](/rest-apis/core-api/transactions/get-transaction)

***

## Sample transaction

In a successful PayPal withdrawal, the origin is the user's `account` and the destination is the external account representing the user's PayPal account.

```json [expandable] theme={null}
{
  "transaction": {
    "id": "9b2f4a17-5e3c-4d77-a8e1-9bcdef2c0a42",
    "origin": {
      "asset": "USD",
      "amount": "15.00",
      "node": {
        "type": "account",
        "id": "b8618cd1-ccb0-4a72-985f-3bb0d268dabd",
        "ownerId": "48e40cb2-6c34-44ce-b2f1-6adac459bb37"
      }
    },
    "destination": {
      "asset": "USD",
      "amount": "14.00",
      "node": {
        "type": "external-account",
        "id": "90acb64a-510d-f9d1-b542-d44e6c53eb5d",
        "ownerId": "48e40cb2-6c34-44ce-b2f1-6adac459bb37"
      }
    },
    "status": "completed",
    "quotedAt": "2025-06-18T00:55:39Z",
    "createdAt": "2025-06-18T00:56:39Z",
    "updatedAt": "2025-06-18T00:57:08Z",
    "denomination": {
      "asset": "USD",
      "amount": "15.00",
      "target": "origin"
    }
  }
}
```

***

## Notify the user

After the transaction completes, display the transaction details to the user so they can confirm the withdrawal succeeded. It must include the **PayPal logo and the PayPal account email used**. Here's an example:

<Frame>
  <div style={{maxWidth: '400px', margin: '0 auto'}}>
    <img src="https://mintcdn.com/uphold-d4756e17/vIioqA6Ud5_sRLRt/developer-guides/apm-transfers/_media/paypal-withdrawal-transaction-completed.png?fit=max&auto=format&n=vIioqA6Ud5_sRLRt&q=85&s=2c8c2ff75bae88edafd33119f5e4c8ca" alt="PayPal transaction completed confirmation" width="1778" height="2384" data-path="developer-guides/apm-transfers/_media/paypal-withdrawal-transaction-completed.png" />
  </div>
</Frame>

***

<Check>You now support PayPal withdrawals via the Payment Widget.</Check>
