Widget Integration Guide
With the SoloPay payment widget, you can integrate payments without building a custom payment UI. The SDK handles wallet connection, signing, and payment processing.
Choose the package that fits your framework.
React Projects
The @solo-pay/widget-react package integrates the widget using a React hook.
Installation
npm install @solo-pay/widget-reactUsage
import { useWidget } from '@solo-pay/widget-react';
function CheckoutButton({ orderId, amount }) {
const { openWidget } = useWidget({
publicKey: 'pk_xxxxx', // Your issued Public Key
defaultPaymentRequest: {
tokenAddress: '0xE4C687167705Abf55d709395f92e254bdF5825a2',
successUrl: 'https://myshop.com/payment/success',
failUrl: 'https://myshop.com/payment/fail',
currency: 'USD',
},
onClose: () => console.log('Widget closed.'),
onError: (err) => console.error('Payment error:', err),
});
return (
<button onClick={() => openWidget({ orderId, amount: String(amount) })}>
Pay Now
</button>
);
}useWidget initializes the SDK instance on mount and automatically cleans up on unmount.
Vanilla JS / Other Frameworks
The @solo-pay/widget-js package works without any framework.
Installation
npm install @solo-pay/widget-jsUsage
import { SoloPay } from '@solo-pay/widget-js';
const solopay = new SoloPay({
publicKey: 'pk_xxxxx',
});
solopay.requestPayment(
{
orderId: 'order-2024-00001',
amount: '25.5',
tokenAddress: '0xE4C687167705Abf55d709395f92e254bdF5825a2',
successUrl: 'https://myshop.com/payment/success',
failUrl: 'https://myshop.com/payment/fail',
currency: 'USD',
},
{
onClose: () => {
// Handle user closing the widget
},
}
);CDN
Use the widget directly via script tag without npm.
<script src="https://cdn.jsdelivr.net/npm/@solo-pay/widget-js/dist/widget.min.js"></script>
<script>
const solopay = new SoloPay({ publicKey: 'pk_xxxxx' });
solopay.requestPayment({
orderId: 'order-2024-00001',
amount: '25.5',
tokenAddress: '0xE4C687167705Abf55d709395f92e254bdF5825a2',
successUrl: 'https://myshop.com/payment/success',
failUrl: 'https://myshop.com/payment/fail',
currency: 'USD',
});
</script>amount and currency Behavior
How amount is interpreted depends on whether currency is provided.
currency | amount interpretation |
|---|---|
Provided (e.g., 'USD', 'KRW') | Fiat amount — automatically converted using the latest 1-hour average price (TWAP) |
| Omitted | Token amount directly — used as-is, no conversion |
Payer method selection: omit tokenAddress and provide currency so the widget lists enabled tokens (with amount + exchange rate). Pass tokenAddress to keep a fixed token.
Example 1: USD-based payment (currency provided)
// amount: 25.5 USD → converted to token amount at the latest 1-hour average price
solopay.requestPayment({
orderId: 'order-001',
amount: '25.5',
currency: 'USD',
tokenAddress: '0xE4C687167705Abf55d709395f92e254bdF5825a2',
successUrl: 'https://myshop.com/payment/success',
failUrl: 'https://myshop.com/payment/fail',
});Example 1b: Let payer choose token (omit tokenAddress)
solopay.requestPayment({
orderId: 'order-001',
amount: '25.5',
currency: 'USD',
// tokenAddress omitted → widget shows enabled methods
successUrl: 'https://myshop.com/payment/success',
failUrl: 'https://myshop.com/payment/fail',
});Example 2: Token amount directly (currency omitted)
// amount: 25.5 USDT directly (no conversion)
solopay.requestPayment({
orderId: 'order-001',
amount: '25.5',
// currency omitted → amount is used as token amount directly
tokenAddress: '0xE4C687167705Abf55d709395f92e254bdF5825a2',
successUrl: 'https://myshop.com/payment/success',
failUrl: 'https://myshop.com/payment/fail',
});What happens when currency is omitted?
When currency is omitted, amount is treated as the token amount directly. For example, passing amount: '25.5' with a USDT token requests exactly 25.5 USDT. Use this when no currency conversion is needed.
How It Works
- Desktop: The widget opens as a popup window.
- Mobile: The user is redirected to a full-screen page.
- After payment completion or failure, the widget auto-redirects to
successUrlorfailUrl. - When both are
solopay:none, the widget does not redirect and ends on its completion screen.
Returning from a mobile wallet
- Android browser: returns to the active widget after wallet approval.
- iOS browser: the payer switches back manually. Automatic HTTPS return is disabled because each wallet request may open another browser tab.
- Native app: pass the app return URL in
deeplinktorequestPayment()orrequestApproval().
solopay.requestPayment({
orderId: 'order-001',
amount: '25.5',
tokenAddress: '0x...',
successUrl: 'myapp://payment/success',
failUrl: 'myapp://payment/fail',
deeplink: 'myapp://payment',
});Browser integrations should omit deeplink to keep the platform's default wallet-return behavior.
Callback URL Handling
After payment, SoloPay redirects the user to the successUrl or failUrl specified at payment creation. The widget automatically appends paymentId, orderId, and status as query parameters.
| Parameter | Description |
|---|---|
paymentId | The unique payment identifier |
orderId | The merchant order ID |
status | Payment result: success, fail, or closed |
https://myshop.com/payment/success?paymentId=0xabc123...&orderId=order-001&status=success
https://myshop.com/payment/fail?paymentId=0xabc123...&orderId=order-001&status=fail
https://myshop.com/payment/fail?paymentId=0xabc123...&orderId=order-001&status=closedDo Not Trust URL Parameters
URL parameters can be manipulated by the user. Always verify payment status via API as the final check.
Skipping the redirect (QR, kiosk, POS)
When there is no merchant page to return to — a customer scanning a QR in the store and paying on their own phone — pass solopay:none as both successUrl and failUrl at payment creation.
{
"orderId": "order-2024-00001",
"amount": 12000,
"currency": "KRW",
"tokenAddress": "0xE4C687167705Abf55d709395f92e254bdF5825a2",
"successUrl": "solopay:none",
"failUrl": "solopay:none"
}The widget then behaves as follows.
- It stops on the completion screen, still showing the amount and transaction hash
- The button reads "Close" instead of "Return to Merchant" and only closes the tab
- The cancel button is not rendered
Both fields are required and must parse as URLs, so an empty string or a bare none is rejected by the gateway. solopay:none is a valid URI and passes.
With no redirect there is no callback URL, so the result must be confirmed through a webhook or the status API.
Payment Verification (Required)
As soon as the paymentId is received from the callback URL, call the status API to verify payment. The GET /payments/:id endpoint uses the x-public-key header, which can be called from the browser.
const response = await fetch(`https://gateway.dev.solonetwork.io/api/v1/payments/0xabc123...`, {
headers: { 'x-public-key': 'pk_xxxxx' },
});
const result = await response.json();Common Verification Checklist
- [ ] Confirm
status === 'PAID'(payment success) - [ ] Confirm
tokenAddressmatches the expected token - [ ] Confirm
orderIdmatches the expected orderId - [ ] Prevent duplicate completion processing for the same
paymentId
Amount verification — what to compare depends on whether currency was used when creating the payment.
The widget runs client-side, so the amount can be tampered with. Always compare against the expected amount stored in your order database.
When the payment was created with currency (fiat-based):
- [ ]
currencymatches the currency code stored with the order - [ ]
fiatAmountmatches the fiat amount stored with the order - [ ] (Optional)
tokenPriceis within an acceptable range — guards against price swings
When the payment was created without currency (direct token amount):
- [ ]
amount(wei) matches the expected token amount in wei — the responseamountis always in wei, so convert your expected value withparseUnits(expected, tokenDecimals)before comparing.
amount is always in wei
The response amount field is always in wei, regardless of whether currency was used. When currency is provided, the user's fiat input is stored in fiatAmount, while amount holds the converted wei value. Do not compare amount directly against a fiat value.
Webhook Integration Recommended
Callbacks are browser-redirect based and can be lost due to network issues. Using it with Webhooks allows reliable payment completion reception. View Webhook Setup Guide
Approve-Only Mode
The widget supports an approve-only mode that performs only the ERC-20 token approval step without executing a payment. This is useful for:
- Pre-authorizing token spending before the actual payment
- Faster checkout — users approve once, then future payments skip the approval step
- Recurring payments — approve once with a high allowance, pay multiple times
React
import { useWidget } from '@solo-pay/widget-react';
function PreApproveButton() {
const { openApproval } = useWidget({
publicKey: 'pk_xxxxx',
onClose: () => console.log('Widget closed.'),
onError: (err) => console.error('Error:', err),
});
return (
<button onClick={() => openApproval({
tokenAddress: '0xE4C687167705Abf55d709395f92e254bdF5825a2',
successUrl: 'https://myshop.com/approve/success',
failUrl: 'https://myshop.com/approve/fail',
})}>
Pre-Approve Token
</button>
);
}Vanilla JS
const solopay = new SoloPay({ publicKey: 'pk_xxxxx' });
solopay.requestApproval({
tokenAddress: '0xE4C687167705Abf55d709395f92e254bdF5825a2',
successUrl: 'https://myshop.com/approve/success',
failUrl: 'https://myshop.com/approve/fail',
});Callback URL Parameters (Approve-Only)
After approval, the widget redirects to successUrl or failUrl with these query parameters:
| Parameter | Description |
|---|---|
status | Result: approved or closed |
tokenAddress | The token contract address that was approved |
txHash | Approval transaction hash (only on success) |
https://myshop.com/approve/success?status=approved&tokenAddress=0x...&txHash=0x...
https://myshop.com/approve/fail?status=closed&tokenAddress=0x...Next Steps
- Webhook Setup — Reliable payment completion notifications