Skip to content

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

bash
npm install @solo-pay/widget-react

Usage

typescript
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

bash
npm install @solo-pay/widget-js

Usage

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

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

currencyamount interpretation
Provided (e.g., 'USD', 'KRW')Fiat amount — automatically converted using the latest 1-hour average price (TWAP)
OmittedToken amount directly — used as-is, no conversion

Example 1: USD-based payment (currency provided)

typescript
// 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 2: Token amount directly (currency omitted)

typescript
// 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 successUrl or failUrl.

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.

ParameterDescription
paymentIdThe unique payment identifier
orderIdThe merchant order ID
statusPayment 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=closed

Do Not Trust URL Parameters

URL parameters can be manipulated by the user. Always verify payment status via API as the final check.

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.

typescript
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 tokenAddress matches the expected token
  • [ ] Confirm orderId matches 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):

  • [ ] currency matches the currency code stored with the order
  • [ ] fiatAmount matches the fiat amount stored with the order
  • [ ] (Optional) tokenPrice is 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 response amount is always in wei, so convert your expected value with parseUnits(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

typescript
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

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

ParameterDescription
statusResult: approved or closed
tokenAddressThe token contract address that was approved
txHashApproval 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

Non-custodial Web3 payment infrastructure for ERC-20 checkout, sponsored gas, and wallet-to-wallet settlement.