By the end of this guide the Payment Widget will be running in your app, mounted to a container, and emitting events you can react to.
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 — either a web app (iframe) or a native app (WebView) — to embed the Widget.
The Widget runs from one of two hosts depending on environment:
| Environment | Widget host |
|---|
| Sandbox | https://payment-widget.enterprise.sandbox.uphold.com |
| Production | https://payment-widget.enterprise.uphold.com |
Setup
1. 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.
npm install @uphold/enterprise-payment-widget-web-sdk
2. Allow the Widget domain in your CSP
The Widget loads in an iframe. If your app uses a Content Security Policy, allow the Widget host for your environment(s) under frame-src.
<meta
http-equiv="Content-Security-Policy"
content="frame-src 'self' https://payment-widget.enterprise.sandbox.uphold.com https://payment-widget.enterprise.uphold.com;"
>
If your app does not use CSP, skip this step.
3. 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.
Never call the Create Session endpoint from the client. Your Client Secret must not leave your backend.
Call POST /widgets/payment/sessions with the desired flow (select-for-deposit, select-for-withdrawal, or authorize) and the user the session is for:
curl -X POST https://api.sandbox.uphold.com/widgets/payment/sessions \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-On-Behalf-Of: user $USER_ID" \
-H "Content-Type: application/json" \
-d '{ "flow": "select-for-deposit" }'
The response wraps the session object:
{
"session": {
"flow": "select-for-deposit",
"url": "https://payment-widget.enterprise.sandbox.uphold.com/...",
"token": "..."
}
}
Pass response.session to your frontend (e.g. as part of your page response or via your own API endpoint). The SDK takes it as a single session argument.
On the frontend, instantiate PaymentWidget with the session from Step 3, then mount it into a container element.
import { PaymentWidget } from '@uphold/enterprise-payment-widget-web-sdk';
// session is `response.session` from your backend in Step 3
const widget = new PaymentWidget(session, {
paymentMethods: [
{ type: 'card' },
{ type: 'bank' },
{ type: 'crypto', assets: { include: ['BTC', 'ETH', 'XRP'] } }
],
theme: { appearance: 'dark' }, // omit to follow system preference
debug: true // verbose logging during development
});
widget.mountIframe(document.getElementById('payment-container'));
The container must have explicit CSS width and height — the iframe fills its bounds. Minimum recommended size is 400px × 600px.
For type inference on the complete event, pass the flow as a generic: new PaymentWidget<'select-for-deposit'>(session). See the SDK reference for full constructor details.
The Widget emits four events during its lifecycle. Wire up handlers before calling mountIframe.
| Event | Fires when | What to do |
|---|
ready | The Widget has finished loading | Hide your loading state |
complete | The user finished selection or authorization | Read event.detail.value, then call widget.unmount() |
cancel | The user dismissed the Widget | Call widget.unmount(), return them to your flow |
error | The Widget hit an unrecoverable error | Read event.detail.error, call widget.unmount(), retry |
The Widget does not unmount itself. You must call widget.unmount() from complete, cancel, and error handlers.
widget.on('ready', () => {
console.log('Payment Widget is ready');
});
widget.on('complete', (event) => {
console.log('Selection:', event.detail.value);
widget.unmount();
});
widget.on('cancel', () => {
console.log('Payment cancelled');
widget.unmount();
});
widget.on('error', (event) => {
console.error('Payment error:', event.detail.error);
widget.unmount();
});
The shape of event.detail.value varies by flow. See Events in the SDK reference for the full type definitions.
6. Test in Sandbox
With your Sandbox credentials and the Sandbox Widget host configured, run through this checklist:
- The Widget mounts and
ready fires.
- Selecting a payment method fires
complete with the expected event.detail.value shape for your flow.
- Closing or dismissing the Widget fires
cancel.
- The browser console shows no CSP violations (look for “Refused to frame”).
Once Sandbox is green, swap your OAuth credentials to Production. The Widget host is selected automatically by the session url returned from your backend — no client-side environment switching is needed.
Native app integration
Native mobile apps embed the Widget through a WebView that loads an HTML page hosting the SDK. Events flow between native code and the WebView through a JavaScript bridge.
The setup is the same as the web flow above — install the SDK in your JS bundle, create the session on your backend, and instantiate PaymentWidget. The only addition is the bridge that forwards events to native code.
WebView HTML template
Bundle this HTML with your app and load it in the WebView. The sendToNativeApp helper at the bottom forwards events to whichever bridge is available (iOS, Android, or React Native).
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Payment Widget</title>
<style>
body { margin: 0; padding: 0; }
#payment-container { width: 100%; height: 100vh; }
</style>
</head>
<body>
<div id="payment-container"></div>
<!-- Include your JS bundle which contains the SDK -->
<script src="your-bundle-with-sdk.js"></script>
<script>
window.addEventListener('load', () => {
const session = createPaymentWidgetSession();
const widget = new PaymentWidget(session);
widget.on('complete', (event) => {
sendToNativeApp('complete', event.detail);
widget.unmount();
});
widget.on('cancel', () => {
sendToNativeApp('cancel');
widget.unmount();
});
widget.on('error', (event) => {
sendToNativeApp('error', event.detail.error);
widget.unmount();
});
widget.mountIframe(document.getElementById('payment-container'));
});
function sendToNativeApp(type, data = null) {
const message = { type, data };
if (window.webkit?.messageHandlers?.paymentWidgetMessage) {
window.webkit.messageHandlers.paymentWidgetMessage.postMessage(message);
} else if (window.PaymentBridge) {
window.PaymentBridge.onMessage(JSON.stringify(message));
} else if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify(message));
}
}
</script>
</body>
</html>
The SDK must be included in your WebView bundle (e.g. via your build pipeline). Loading it from a CDN is not supported.
iOS (Swift)
Android (Java/Kotlin)
React Native
import WebKit
class PaymentViewController: UIViewController, WKScriptMessageHandler {
@IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// Register a single message handler for all Widget events
let contentController = webView.configuration.userContentController
contentController.add(self, name: "paymentWidgetMessage")
if let url = Bundle.main.url(forResource: "payment-widget", withExtension: "html") {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == "paymentWidgetMessage",
let messageDict = message.body as? [String: Any],
let type = messageDict["type"] as? String else {
return
}
let data = messageDict["data"]
switch type {
case "complete": handlePaymentComplete(data: data)
case "cancel": handlePaymentCancel()
case "error": handlePaymentError(error: data)
default: print("Unknown Payment Widget message type: \(type)")
}
}
private func handlePaymentComplete(data: Any?) { /* navigate to success */ }
private func handlePaymentCancel() { /* return user to previous screen */ }
private func handlePaymentError(error: Any?) { /* show error UI */ }
deinit {
webView?.configuration.userContentController.removeScriptMessageHandler(forName: "paymentWidgetMessage")
}
}
import android.webkit.WebView;
import android.webkit.WebSettings;
import android.webkit.JavascriptInterface;
import android.util.Log;
import org.json.JSONObject;
WebView webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new PaymentBridge(), "PaymentBridge");
webView.loadUrl("file:///android_asset/payment-widget.html");
public class PaymentBridge {
@JavascriptInterface
public void onMessage(String messageJson) {
try {
JSONObject message = new JSONObject(messageJson);
String type = message.getString("type");
Object data = message.opt("data");
runOnUiThread(() -> {
switch (type) {
case "complete": handlePaymentComplete(data != null ? data.toString() : null); break;
case "cancel": handlePaymentCancel(); break;
case "error": handlePaymentError(data != null ? data.toString() : null); break;
default: Log.w("Payment", "Unknown Payment Widget message type: " + type);
}
});
} catch (Exception e) {
Log.e("Payment", "Error parsing Payment Widget message", e);
}
}
private void handlePaymentComplete(String data) { /* navigate to success */ }
private void handlePaymentCancel() { /* return user to previous screen */ }
private void handlePaymentError(String error) { /* show error UI */ }
}
If you load local assets, ensure the WebView allows file access according to your security requirements (e.g. setAllowFileAccess(true) when needed).
import { WebView } from 'react-native-webview';
import { Alert } from 'react-native';
const PaymentScreen = () => {
const handleMessage = (event) => {
try {
const message = JSON.parse(event.nativeEvent.data);
switch (message.type) {
case 'complete': handlePaymentComplete(message.data); break;
case 'cancel': handlePaymentCancel(); break;
case 'error': handlePaymentError(message.data); break;
default: console.log('Unknown message type:', message.type);
}
} catch (error) {
console.error('Error parsing WebView message:', error);
}
};
const handlePaymentComplete = (data) => { /* navigate to success */ };
const handlePaymentCancel = () => { /* return user to previous screen */ };
const handlePaymentError = (error) => { /* show error UI */ };
return (
<WebView
source={{ uri: 'file:///path/to/payment-widget.html' }}
javaScriptEnabled={true}
domStorageEnabled={true}
onMessage={handleMessage}
/>
);
};
For iOS, file:// URLs may be restricted. Consider source={{ html: '<html>...</html>' }} or a bundled asset, adjusted per platform.
Configuration reference
The most common SDK options. See the SDK reference for the full schema and all event types.
| Option | Purpose |
|---|
paymentMethods | Filter which payment methods (and assets) appear. Omit to show all. |
theme | Customize the Widget’s appearance: force light or dark mode, set brand colors, typography, and per-component border radii. See WidgetThemeOption for all options. |
debug | Verbose console logging. Use during development. |
maxAccountsPerAsset | Cap accounts created per asset for crypto deposit flows (max effective: 100). |
Troubleshooting
Widget not displaying
Confirm the Widget host for your environment is in the frame-src directive of your CSP (see Step 2). 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.
Events not firing in native apps
Verify that:
- 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.
Widget never unmounts. The SDK does not auto-unmount. Call widget.unmount() from each terminal handler (complete, cancel, error).
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: