Create Payment
Create a payment and receive a unique payment ID.
Overview
When using the SoloPay widget, payment creation is handled automatically by the widget. This page is a reference API spec for understanding the internals or for custom implementations.
Created payments expire automatically after 5 minutes.
- Auth:
x-public-keyheader required (pk_xxx) - Chain and recipient address are determined by merchant configuration
tokenAddressmust be whitelisted and enabled for the merchant
REST API
curl -X POST https://gateway.dev.solonetwork.io/api/v1/payments \
-H "x-public-key: pk_xxxxx" \
-H "Origin: https://yourshop.com" \
-H "Content-Type: application/json" \
-d '{
"orderId": "order-001",
"amount": 10.5,
"tokenAddress": "0xE4C687167705Abf55d709395f92e254bdF5825a2",
"successUrl": "https://example.com/success",
"failUrl": "https://example.com/fail"
}'Request Parameters
| Field | Type | Req. | Description |
|---|---|---|---|
orderId | string | ✓ | Merchant order ID (no duplicates per merchant) |
amount | number | ✓ | Payment amount (token units or fiat units) |
tokenAddress | address | ✓ | ERC-20 token contract address (whitelisted & enabled for merchant) |
successUrl | string | ✓ | Redirect URL on success (accepts solopay:none) |
failUrl | string | ✓ | Redirect URL on failure (accepts solopay:none) |
currency | string | Fiat currency code (e.g., USD, KRW). Triggers price conversion |
currency option
When currency is provided, amount is treated as a fiat amount. The server automatically converts it using the latest 1-hour average price (TWAP) to determine the token amount. Example: amount: 10, currency: "USD" → pays the token amount equivalent to 10 USD based on the past 1-hour average price.
The 1-hour average dampens short-term price swings. The tokenPrice field in the response reflects the 1-hour average used for the conversion.
Payments with nowhere to return to (QR, kiosk, POS)
When a customer scans a QR shown in the store and pays on their own phone, that browser never came from your site. Redirecting it sends the customer to a page they have never seen.
Pass solopay:none in both fields instead. The widget stops on its own completion screen rather than redirecting, and its button closes the tab instead of returning to the merchant. The cancel button is not shown at all.
Both fields are required and must parse as URLs, so an empty string or a bare none is rejected. solopay:none is a valid URI and passes validation.
With no redirect there is no callback URL. Confirm the result through a webhook or the status API.
Response
Success (201 Created)
{
"success": true,
"data": {
"paymentId": "0xabc123def456...",
"orderId": "order-001",
"chainId": 80002,
"tokenAddress": "0xE4C687167705Abf55d709395f92e254bdF5825a2",
"tokenSymbol": "SUT",
"tokenDecimals": 18,
"gatewayAddress": "0x...",
"forwarderAddress": "0x...",
"amount": "10500000000000000000",
"recipientAddress": "0xMerchantWallet...",
"merchantId": "0x...",
"deadline": "1706281200",
"successUrl": "https://example.com/success",
"failUrl": "https://example.com/fail",
"expiresAt": "2024-01-26T12:35:00.000Z",
"tokenPermitSupported": true,
"currency": "USD",
"fiatAmount": 10.5,
"tokenPrice": 1.0
}
}Error Responses
| HTTP | Code | Cause |
|---|---|---|
| 400 | TOKEN_NOT_ENABLED | Token is not enabled for this merchant |
| 404 | TOKEN_NOT_FOUND | Token not in whitelist |
| 400 | UNSUPPORTED_CHAIN | Unsupported chain |
| 400 | CHAIN_NOT_CONFIGURED | Merchant has no chain configured |
| 400 | RECIPIENT_NOT_CONFIGURED | Merchant recipient address not configured |
| 400 | VALIDATION_ERROR | Input validation failed |
| 409 | DUPLICATE_ORDER | orderId already used |
Response Fields
| Field | Type | Description |
|---|---|---|
paymentId | string | Unique payment identifier (bytes32 hash) |
amount | string | Amount in wei |
gatewayAddress | address | PaymentGateway contract address |
forwarderAddress | address | ERC2771 Forwarder address (for Gasless) |
merchantId | string | Merchant ID (bytes32) |
deadline | string | Server signature deadline (Unix timestamp); required for pay() and gasless. Default: 1 hour (3600s) |
expiresAt | datetime | Payment expiry (5 minutes from creation) |
tokenPermitSupported | boolean | Whether the token supports EIP-2612 Permit |
currency | string | Fiat currency code (included only when requested) |
fiatAmount | number | Original fiat amount (included only when requested) |
tokenPrice | number | Latest 1-hour average token price (TWAP) used for conversion (included only when requested) |
When Using the Widget
When using the widget (@solo-pay/widget-js / @solo-pay/widget-react), there is no need to call this API directly — the widget handles it automatically.
Next Steps
- Payment Status - Check payment progress
- How Payments Work - Gasless architecture