Before you start
You’ll need:- Access to Widgets API to create widget sessions. Manage your access in Enterprise Portal.
- A backend that can call the Widgets API to create sessions on behalf of your users.
- A frontend — a web app, or a native app with a WebView — to embed the Widget.
Choose an integration approach
We recommend integrating without the SDK for native apps — it’s simpler to set up. For web-only integrations, the Web SDK is a solid default. There are two ways to embed the Widget:- Web SDK — Install
@uphold/enterprise-payment-widget-web-sdkfor a better developer experience — typed events and a more streamlined integration on web. For native apps, the SDK must be bundled into the WebView’s HTML page, and if your flow needs Apple Pay or Google Pay, that page must be served from a real HTTPS origin rather than bundled locally. - Without the SDK — Listen for the Widget’s messages over the iframe or WebView and respond to them directly. This is also the simpler option for native apps: there’s no SDK to bundle into the WebView’s HTML page, so payment methods like Apple Pay and Google Pay work without any extra setup.
ready, complete, cancel, error).
Shared setup
These two steps are identical whichever approach you choose above — do them once, then jump to the matching section below.1. Create a session on your backend
The Payment Widget runs against a session — a short-lived, server-side authorization scoped to one flow and one user. Create it server-side using your OAuth credentials.To create a session, you must have the
Payment Widget scope.Create session with the desired flow (select-for-deposit, select-for-withdrawal, or authorize), the user the session is for, and, optionally, the Widget options:
options object accepts the same options as the SDK, except debug — see Configuration reference.
The response wraps the session object:
response.session to your frontend, for example in your page response or through your own API endpoint.
Keep session.url unmodified, including its query string. The only change allowed is adding theme_appearance when integrating without the SDK (see Theme and layout setup). Then:
- With the Web SDK, pass
response.sessionas thesessionargument to thePaymentWidgetconstructor — see Setup with Web SDK. - Without the SDK, load
session.urldirectly into your iframe or WebView — see Setup without the SDK.
2. Allow the Widget domain in your CSP
If your web app embeds the Widget in an iframe and enforces a Content Security Policy, allow the Widget host for your environment(s) underframe-src.
If your app does not use CSP, skip this step.
Setup with Web SDK
Install the SDK
Install the SDK in the frontend that will host the Widget — your web app, or the JS bundle loaded by your native WebView.Initialize and mount the Widget
On the frontend, instantiatePaymentWidget with the session from Create a session on your backend, then mount it into a container element. If you set theme, layout, or authorize.mode in Create session, also pass the same values to the constructor, to avoid a brief change in appearance (see Theme and layout setup).
The container must have explicit CSS width and height — the iframe fills its bounds. Minimum recommended size is 400px × 600px.
complete event, pass the flow as a generic: new PaymentWidget<'select-for-deposit'>(session). See the SDK reference for full constructor details.
Handle Widget events
The Widget emits four events during its lifecycle. Wire up handlers before callingmountIframe.
event.detail.value varies by flow. See Events in the SDK reference for the full type definitions.
Native apps with the Web SDK
Bundle the Web SDK into a WebView (not recommended — see Setup without the SDK instead)
Bundle the Web SDK into a WebView (not recommended — see Setup without the SDK instead)
Native mobile apps can also use the SDK to embed the Widget through a WebView, building on the Install the SDK and Initialize and mount the Widget steps above. To do so, it requires you to bundle the SDK into the WebView’s HTML page, and forward events to native code through a bridge. We recommend integrating without the SDK for native apps as it has a simpler setup, but if you choose to use the SDK, follow the steps below.Create a JS bundle that includes the SDK. This bundle will be loaded in the WebView. You can use a tool like Webpack or Rollup to bundle the SDK and your custom code into a single JS file.Then create an HTML page that includes the JS bundle and mounts the Widget. This page will be loaded in the WebView. Here is an example you can use — the Here is a sample of how to create a WebView in your native app and load the HTML page that contains the SDK and mounts the Widget. The WebView should be configured to allow JavaScript execution and to forward messages to your native code.
Loading the Web SDK from a CDN is not supported.
sendToNativeApp helper at the bottom forwards events to whichever bridge is available (iOS, Android, or React Native).Platform setup
- iOS (Swift)
- Android (Java/Kotlin)
- React Native
Setup without the SDK
Instead of installing the SDK, you can load the Widget’s sessionurl directly — as an iframe you create yourself on web, or as your WebView’s top-level page on native — and speak its underlying message protocol. This approach is recommended for native apps as there is no need for bundling the SDK into the WebView’s HTML page and serving that page from a real HTTPS origin to avoid issues with Apple Pay/Google Pay.
The Shared setup steps still apply here — you just don’t install anything. Only how you load the Widget and exchange messages changes.
The message protocol
These are the message types the Widget speaks. The transport carrying them differs per platform — see the tabs below. From the Widget to your host:
From your host to the Widget:
The Widget loads its session on its own, using the options set when creating the session, so the
init reply is optional. The init payload carries an options object with the same shape as the SDK’s PaymentWidgetOptions, except debug — omit it or pass {} to use defaults. For now, include the same theme, layout, and authorize.mode you set in Create session, to avoid a brief change in appearance (see Theme and layout setup).
If you send init, its options are merged with the ones set when creating the session, and Create session values take precedence for any value set in both.
Sending the full session object in
init ({ ...session, options }) still works but is deprecated. The Widget ignores it if it has already loaded its session, and logs a console warning when it’s used. Send { options } only.Platform implementation
- Web
- iOS (Swift)
- Android (Kotlin)
- React Native
Mount the session
url in an iframe you create yourself, and exchange messages over the standard window.postMessage API. Make sure the iframe’s allow attribute includes clipboard permissions (and payment permissions, for Apple Pay and Google Pay), and that its container has explicit CSS width and height (minimum recommended size is 400px × 600px).Theme and layout setup
Settheme, layout, and authorize.mode in the Create session options to customize the Widget’s appearance and authorization UI. See WidgetThemeOption, WidgetLayout, and AuthorizeFlowOptions for all options.
The Widget applies the options set in Create session once it has loaded the session. Until then, it renders its defaults: the browser or OS color scheme, the boxed layout, and, in the authorize flow, the default authorization UI. If you set theme, layout, or authorize.mode, the Widget may briefly show those defaults before switching.
Until a future release removes the need for it, pass the same theme, layout, and authorize.mode from your frontend as well. Use the same values in both places, since Create session values replace the frontend ones once the session loads.
With the Web SDK
Pass the sametheme, layout, and authorize.mode in the constructor options:
Without the SDK
-
Before loading
session.url, append atheme_appearancequery parameter with the value you want (darkorlight). The Widget renders before it receives any options, so this makes its first render use the right light or dark mode instead of the browser or OS color scheme:This works the same whether you load the result into a web iframe or as your native WebView’s top-level page.Addtheme_appearancethroughURL/URLSearchParams, as above, to preserve thesessionTokenquery parameter the Widget needs to load its session. Don’t rebuild the query string by hand. -
When the Widget sends its
loadmessage, reply with aninitmessage carrying the sametheme,layout, andauthorize.mode:
Test in Sandbox
With your Sandbox credentials and the Sandbox Widget host configured, run through this checklist — it applies whichever approach you integrated with:- The Widget mounts and
readyfires. - Selecting a payment method fires
completewith the expected value for your flow —event.detail.valuewith the Web SDK, or thevaluepayload of thecompletemessage without it. - Closing or dismissing the Widget fires
cancel. - If your app uses a CSP (see Shared setup), the browser console shows no violations (look for “Refused to frame”).
url returned from your backend — no client-side environment switching is needed.
Configuration reference
The most common Widget options. See the SDK reference for the full schema and all event types. If you’re integrating without the SDK, these map directly to theoptions object you send in the Create session request, or in your init reply, except debug, which only applies to the SDK.
Passing paymentMethods or maxAccountsPerAsset from your frontend, in the SDK constructor or the init reply, is deprecated. Set them in the Create session request only.
Troubleshooting
Widget not displaying Confirm the Widget host for your environment is in theframe-src directive of your CSP (see Shared setup). Open DevTools → Console and look for Refused to frame violations.
Container is empty after mount
The iframe fills its container — the container must have explicit CSS width and height. Minimum recommended size is 400px × 600px.
Widget briefly shows the wrong theme or layout
The Widget renders its defaults (the browser or OS color scheme, the boxed layout, and, in the authorize flow, the default authorization UI) until it loads the session, then switches to the theme, layout, and authorize.mode set in Create session. Verify that:
- You pass the same
theme,layout, andauthorize.modefrom your frontend, with the same values as in Create session. - Without the SDK, you append
theme_appearancetosession.urlbefore loading it, and reply toloadwithinitas soon as you receive it.
- JavaScript is enabled in the WebView.
- The message bridge is registered before the HTML page loads.
- Event handler names match the platform-specific bridge contract used in
sendToNativeApp.
- Your message-handler / listener name is exactly
uphdPaymentWidget— the name the Widget checks for on both iOS and Android — and is registered before the WebView loads the sessionurl. - On web, you’re filtering incoming
messageevents by origin (event.origin === sessionOrigin) — messages from other origins should be ignored, not treated as Widget events.
error
The Widget couldn’t load its session. Usually, the sessionToken in session.url is expired, invalid, or already used, or session.url was modified or rebuilt without its query string. Create a new session and load its session.url unmodified.
Widget never unmounts. The Web SDK does not auto-unmount, and neither does the Widget itself when integrating without it. Call widget.unmount() (Web SDK) or tear down your iframe/WebView (without the SDK) from each terminal message (complete, cancel, error).
Apple Pay option not shown to the user
Confirm the device supports Apple Pay through the Payment Request API in case of a deposit or for the Disbursement Request API in case of a withdrawal and that the user has the required capabilities enabled. If you mounted the iframe yourself instead of using mountIframe(), confirm its allow attribute includes payment 'src' — without it, the Widget can’t detect Apple Pay support and hides the option. Also confirm your domain is registered with Apple Pay (see Apple Pay); an unverified domain makes Apple Pay silently unavailable.
Apple Pay sheet doesn’t open when the button is clicked (authorize flow)
This is usually the device, not the integration: Apple Pay requires the device to be able to authenticate the user at the moment of the click. For example, on a MacBook with the lid closed, Touch ID is unreachable and the sheet won’t open; the same applies if the device has no Touch ID/Face ID or passcode configured. Ask the user to check their device’s authentication method is available, then try again.
Apple Pay sheet opens, then is immediately dismissed
This is typically a merchant validation failure — check that your domain, and any ancestor frame domains, are registered with Apple Pay under Uphold’s merchant ID (see Apple Pay).
Next steps
- Review the complete SDK Reference for all available methods and events.
- Read our Developer Guides for step-by-step instructions on implementing specific payment methods with the Widget: