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

# Apple Pay deposit via the REST API

> Fund a user's account from Apple Pay through the Uphold REST API.

export const apmLabel_3 = "Apple Pay"

export const apmLabel_2 = "Apple Pay"

export const direction_0 = "deposit"

export const apmLabel_1 = "Apple Pay"

export const apmLabel_0 = "Apple Pay"

This guide covers funding a user's account from Apple Pay using the REST API together with the Payment Widget. Every deposit runs through the widget's **Authorize flow**, where the user authorizes with Apple Pay. Your backend only creates the quote and the widget session.

## Prerequisites

* The user has [completed onboarding](/developer-guides/user-onboarding/overview).
* The `apple-pay` capability is enabled.
* The Payment Widget is [set up in your frontend](/widgets/payment/installation-and-setup).
* [Apple Pay's additional setup requirements](/developer-guides/apm-transfers/overview#apple-pay) are met.
* The target browser or platform [supports Apple Pay](/developer-guides/apm-transfers/overview#browser-and-platform-compatibility-payment-widget).

## Walkthrough

The diagram shows an Apple Pay transaction 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 deposit
  U->>B: Get rails and capabilities
  B->>A: GET /core/rails?type=apm
  A-->>B: { rails }
  B->>A: GET /core/capabilities
  A-->>B: { capabilities }
  B-->>U: { rails, capabilities }
  U->>B: Get accounts
  B->>A: GET /core/accounts
  A-->>B: { accounts }
  B-->>U: { accounts }
  Usr->>U: Select Apple Pay and destination account
  Usr->>U: Choose amount
  U->>B: Create quote
  B->>A: Create quote (APM → account)
  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: Authorize Apple Pay
  P->>A: Create transaction
  A-->>P: { transaction }
  P-->>U: complete { transaction, trigger }
  B-->>Usr: Notify the user
```

***

## Check available rails

Call [List rails](/rest-apis/core-api/assets/list-rails) to verify Apple Pay deposit is available.

```http theme={null}
GET /core/rails?type=apm
```

```json theme={null}
{
  "rails": [
    {
      "type": "apm",
      "network": "apple-pay",
      "method": "apple-pay",
      "asset": "USD",
      "decimals": 2,
      "features": ["deposit", "withdraw"]
    }
  ]
}
```

## Check capabilities

Call [List capabilities](/rest-apis/core-api/capabilities/list-user-capabilities) to confirm the user has the `apple-pay` capability enabled with no unmet requirements.

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

```json theme={null}
{
  "capabilities": [
    {
      "code": "apple-pay",
      "name": "Apple Pay",
      "enabled": true,
      "requirements": [],
      "restrictions": []
    }
  ]
}
```

## Select destination account

{apmLabel_3} deposits can target any account. If the selected account is not in the {apmLabel_3} account's currency, the amount will be converted at settlement using Uphold's prevailing rate. Make sure the destination asset has the necessary [features enabled](/rest-apis/core-api/assets/introduction#features-and-deposits-/-withdrawals).

### Find an existing account

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

```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"
      }
    }
  ]
}
```

### Create a new account

If the user has no accounts, create one with [Create account](/rest-apis/core-api/accounts/create-account) before proceeding.

```http theme={null}
POST /core/accounts
{
  "label": "My USD account",
  "asset": "USD"
}
```

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

***

## Create a quote

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

Specify Apple Pay as the origin with `type: "apm"` and `method: "apple-pay"`.

```http theme={null}
POST /core/transactions/quote
{
  "origin": {
    "type": "apm",
    "method": "apple-pay"
  },
  "destination": {
    "type": "account",
    "id": "71e9fd4b-dfcd-4643-a5b0-51fd33e50a8d"
  },
  "denomination": {
    "asset": "USD",
    "amount": "50",
    "target": "origin"
  }
}
```

A successful response includes the quote details and a `requirements` array. For Apple Pay, it contains `authorize:apple-pay`, which indicates that the user must authorize Apple Pay before the transaction can be created.

```json [expandable] theme={null}
{
  "quote": {
    "id": "c3e8d2f7-9a41-4b75-b8e3-1d6f4a9c2e57",
    "origin": {
      "amount": "50.00",
      "asset": "USD",
      "node": {
        "type": "apm",
        "method": "apple-pay"
      },
      "rate": "1"
    },
    "destination": {
      "amount": "48.75",
      "asset": "USD",
      "node": {
        "type": "account",
        "id": "71e9fd4b-dfcd-4643-a5b0-51fd33e50a8d",
        "ownerId": "48e40cb2-6c34-44ce-b2f1-6adac459bb37"
      },
      "rate": "1"
    },
    "denomination": {
      "asset": "USD",
      "amount": "50.00",
      "target": "origin",
      "rate": "1"
    },
    "fees": [
      {
        "type": "deposit",
        "code": "alternative-payment-method-deposit",
        "asset": "USD",
        "amount": "1.25",
        "percentage": "2.50"
      }
    ],
    "expiresAt": "2025-06-18T01:55:39Z",
    "requirements": [
      "authorize:apple-pay"
    ]
  }
}
```

***

## 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/KbDFogpqirVHaMrx/developer-guides/apm-transfers/_media/apple-pay-deposit-transaction-preview.png?fit=max&auto=format&n=KbDFogpqirVHaMrx&q=85&s=905767efe49db9a79b3dea73bdb8cb8b" alt="Apple Pay order summary" width="1464" height="2912" data-path="developer-guides/apm-transfers/_media/apple-pay-deposit-transaction-preview.png" />
  </div>
</Frame>

***

## Authorize and create the transaction

After the user confirms the Apple Pay quote, hand off to the Payment Widget **Authorize flow**. It presents the Apple Pay sheet, creates the transaction, and polls until a terminal status is reached. Apple Pay authorizes **per transaction** on the user's device — there is no stored authorization to reuse, so the user confirms with Face ID, Touch ID, or their passcode every time. By default, the widget renders a full walkthrough around the authorization; in **headless mode**, it renders only the Apple Pay button so you can embed it directly into your own UI — see [Headless mode](#headless-mode) below.

### Create an authorize session

Call [Create widget session](/rest-apis/widgets-api/payment/create-session) to start the `authorize` flow, passing the `quoteId` and adding `authorize:apple-pay` to the `requirements` array. For web apps, also include the top-page `domain`. To enable [headless mode](#headless-mode), set `authorize.mode` to `headless` in `options`. To customize the theme and layout, see the example [here](/widgets/payment/installation-and-setup#1-create-a-session-on-your-backend).

<Note>
  The top-page domain is the domain shown in the browser's URL bar — the very top-level page hosting the widget iframe. For web apps, it must match the domain you registered with Apple Pay for your merchant ID.
</Note>

```http theme={null}
POST /widgets/payment/sessions
{
  "flow": "authorize",
  "data": {
    "quoteId": "<quoteId>",
    "requirements": [
      "authorize:apple-pay"
    ],
    "domain": "<top-page-domain>" // For web apps only, required for Apple Pay
  }
}
```

The response contains a `session` object with a `url` field that loads the widget. Return `response.session` to your frontend as is.

```json theme={null}
{
  "session": {
    "url": "https://payment-widget.enterprise.uphold.com/?sessionToken=..."
  }
}
```

### Set up the widget

Initialize the widget with the session. The widget presents the {apmLabel_0} sheet, collects device data, creates the transaction, and polls until a terminal status is reached.

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

  // session is `response.session` from your backend
  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 Without SDK [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'; payment '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':
          // Optional: repeat theme, layout, or authorize.mode from Create session to avoid a brief flash of defaults
          iframe.contentWindow.postMessage({ 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 Web SDK](/widgets/payment/installation-and-setup#native-apps-with-the-web-sdk) for the SDK's native-bundling pattern, or [Setup without the SDK](/widgets/payment/installation-and-setup#setup-without-the-sdk) for the no-SDK approach that loads the session `url` directly as the WebView's top-level page.
</Info>

### Headless mode

By default, the widget renders a full authorization experience — a short walkthrough, the {apmLabel_0} button, and a processing screen while the transaction is confirmed. In **headless mode**, it renders only the {apmLabel_0} button, with no surrounding UI, so you can embed it directly into your own layout. Your app then owns the surrounding context and the post-authorization experience — progress and success feedback, and a way to cancel. {apmLabel_0 === 'Google Pay' && `Because there's no surrounding UI to present a 3DS challenge, headless mode is limited to Android devices with cards that support CRYPTOGRAM_3DS authentication.`}

To enable it, set `authorize.mode` to `headless` in the `options` of the [Create widget session](/rest-apis/widgets-api/payment/create-session) request:

```http theme={null}
POST /widgets/payment/sessions
{
  "flow": "authorize",
  "data": { ... }, // Same data as in Create an authorize session
  "options": {
    "authorize": {
      "mode": "headless"
    }
  }
}
```

See [`AuthorizeFlowOptions`](/widgets/payment/sdk-reference#authorizeflowoptions) in the SDK reference for the full type definition.

### 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 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 Without SDK 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 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 {apmLabel_0} authorization could not be completed |
| `card-declined` | The card was declined |
| `card-declined-by-bank` | The card was declined by the issuing bank |
| `card-expired` | The card has expired |
| `card-permanently-declined-by-bank` | The card was permanently declined |
| `card-unauthorized` | The card authorization was not completed |
| `card-unsupported` | The card is not supported for this operation |
| `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 dismisses the {apmLabel_0} sheet or navigates back without completing authorization.

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

  ```javascript Without SDK 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 — except `authorize_transaction_failed`, which is retryable **in headless mode**: the widget resets itself back to its ready state, so the {apmLabel_0} button works again if you leave the widget mounted. In default mode there is no in-place retry for this code — the button does not become usable again, so unmount and create a new authorize session if you want the user to try again.

`authorize_transaction_failed` is the retryable code for {apmLabel_0} authorization and transaction-creation failures. `event.detail.error.details.reason` narrows down where it happened:

<table>
  <thead>
    <tr>
      <th>Reason</th>
      <th>Description</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><code>unable-to-create-payment-request</code></td>
      <td>The browser could not build the {apmLabel_0} payment request</td>
    </tr>

    <tr>
      <td><code>unable-to-make-payment</code></td>
      <td>The device or browser reported it cannot make this payment</td>
    </tr>

    {apmLabel_0 === 'Apple Pay' && (
            <tr>
              <td><code>unable-to-create-merchant-session</code></td>
              <td>Merchant session validation with Apple failed</td>
            </tr>
          )}

    <tr>
      <td><code>unable-to-show-payment-request</code></td>
      <td>The {apmLabel_0} sheet could not be shown</td>
    </tr>

    <tr>
      <td><code>invalid-payment-response</code></td>
      <td>The {apmLabel_0} sheet returned a response without the expected payment token</td>
    </tr>

    <tr>
      <td><code>unable-to-create-transaction</code></td>
      <td>{apmLabel_0} authorization succeeded, but the transaction itself could not be created — see <code>event.detail.error.cause</code> below for the underlying reason</td>
    </tr>
  </tbody>
</table>

Other failures in this flow — such as a missing or expired quote, or the SDK being unavailable — surface with their own `code` and are not retryable; handle those with a generic fallback.

As with any Widget error, the original failure is preserved in [`event.detail.error.cause`](/widgets/payment/sdk-reference#error). When `details.reason` is `unable-to-create-transaction`, `event.detail.error.cause.code` is one of:

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

<CodeGroup>
  ```javascript Web SDK theme={null}
  // Set this to match how you configured the widget — see Headless mode above
  const isAuthorizeHeadless = false;

  widget.on('error', (event) => {
    const { code, cause, details } = event.detail.error;
    console.error('Widget error:', code, details, cause);

    if (code === 'authorize_transaction_failed') {
      if (details.reason === 'unable-to-create-transaction') {
        // The real reason is nested here, not in the top-level `code`
        console.error('Create transaction failed:', cause?.code);
      }

      if (isAuthorizeHeadless) {
        // Retryable in headless mode — keep the widget mounted if you want the user to try again
        return;
      }
      // In default mode there is no in-place retry for this code — fall through and unmount
    }

    widget.unmount();
    // Show a user-friendly error message
  });
  ```

  ```javascript Without SDK theme={null}
  // Inside the onMessage switch from Set up the widget. `isAuthorizeHeadless` is declared once
  // outside the handler (alongside `sessionOrigin`), matching how you configured the widget.
  case 'error': {
    const { code, cause, details } = event.data.error;
    console.error('Widget error:', code, details, cause);

    if (code === 'authorize_transaction_failed') {
      if (details.reason === 'unable-to-create-transaction') {
        console.error('Create transaction failed:', cause?.code);
      }

      if (isAuthorizeHeadless) {
        // Retryable in headless mode — keep the iframe/WebView mounted so the user can try again
        break;
      }
      // In default mode there is no in-place retry for this code — fall through and teardown
    }

    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>

***

## Monitor for settlement

{apmLabel_2} {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 {apmLabel_1} deposit, the origin is represented as an `apm` node with the method as `{apmMethod}`. The destination is the user's `account`.

```json [expandable] theme={null}
{
  "transaction": {
    "id": "9b2f4a17-5e3c-4d77-a8e1-9bcdef2c0a42",
    "origin": {
      "asset": "USD",
      "amount": "50.00",
      "node": {
        "type": "apm",
        "method": "apple-pay"
      }
    },
    "destination": {
      "asset": "USD",
      "amount": "50.00",
      "node": {
        "type": "account",
        "id": "71e9fd4b-dfcd-4643-a5b0-51fd33e50a8d",
        "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": "50.00",
      "target": "origin"
    }
  }
}
```

***

## Notify the user

After the transaction completes, display the transaction details to the user so they can confirm the deposit succeeded. Here's an example:

<Frame>
  <div style={{maxWidth: '400px', margin: '0 auto'}}>
    <img src="https://mintcdn.com/uphold-d4756e17/WuOkmqXUUvhqHoz9/developer-guides/apm-transfers/_media/apple-pay-deposit-transaction-completed.png?fit=max&auto=format&n=WuOkmqXUUvhqHoz9&q=85&s=971a937cd6d1b235110b2ac9efad9eb0" alt="Apple Pay transaction completed confirmation" width="1760" height="2384" data-path="developer-guides/apm-transfers/_media/apple-pay-deposit-transaction-completed.png" />
  </div>
</Frame>

## Troubleshooting

Having issues rendering Apple Pay? See [Troubleshooting](/widgets/payment/installation-and-setup#troubleshooting) in the Payment Widget installation guide.

***

<Check>You now support Apple Pay deposits via the REST API together with the Payment Widget.</Check>
