Coinley handles all the complexity of cryptocurrency payments. You just call our API. Automatic fee splitting, multi-wallet support, and real-time verification included.
Coinley is a payment processor API that lets you accept cryptocurrency payments (USDT, USDC, etc.) without learning blockchain development, smart contracts, or Web3.
Your customers connect their crypto wallet (MetaMask, Coinbase Wallet, etc.), approve the transaction, and payment is processed automatically.
This documentation uses SportsPass - a live ticket sales platform built with Coinley - as the primary example. You'll see exactly how a production application integrates crypto payments without any blockchain expertise required.
→ View the complete SportsPass implementationHere's exactly what happens when a customer pays with cryptocurrency using Coinley. We handle the blockchain complexity - you just call our API.
Your backend calls Coinley API to create payment request
Customer connects MetaMask or any other crypto Wallet to your site
Payment executes via smart contract, Coinley verifies on-chain
Your backend calls POST /api/payments/create
with payment details (amount, currency, network). Coinley returns:
POST /api/payments/create → Returns payment contract details
Your frontend uses Web3.js to connect the customer's crypto wallet and switch to the correct blockchain network:
Use window.ethereum.request() to
connect MetaMask, Coinbase Wallet, or any Web3 wallet.
Use wallet_switchEthereumChain to
switch to Polygon, BSC, Optimism, etc.
The customer approves and executes the payment. Coinley uses smart contracts to automatically split funds:
Customers will see two MetaMask popups. This is normal and required for ERC20 token payments:
After the customer completes payment, your frontend sends the transaction hash to Coinley. We verify the payment on the blockchain and update the payment status:
POST /api/payments/process → Verifies transaction on-chain
Coinley handles all the blockchain complexity: smart contract deployment, fee splitting, network configuration, gas optimization, and payment verification. You just make API calls.
Accept USDT, USDC, and other stablecoins across 8 major blockchains
High fees
~$0.01 fees
~$0.02 fees
~$0.03 fees
~$0.60 fees
~$0.50 fees
Low fees
Coming soon
Pro Tip: Use Polygon or BSC for transactions under $100 to minimize gas fees (~$0.01 vs $3-5 on Ethereum/Optimism)
Use the Coinley SDK for the fastest and easiest integration. The SDK handles all the complexity including wallet connection, network switching, payment execution, and verification.
npm install coinley-pay
# or
yarn add coinley-pay
import { EnhancedSimpleCoinleyPayment } from 'coinley-pay';
import 'coinley-pay/dist/style.css';
Here's a complete example from a real e-commerce checkout:
import { useState } from 'react';
import { EnhancedSimpleCoinleyPayment } from 'coinley-pay';
import 'coinley-pay/dist/style.css';
function CheckoutPage() {
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
const [customerEmail, setCustomerEmail] = useState('');
const [orderTotal] = useState(49.99);
// Handle successful payment
const handleSuccess = (paymentId, transactionHash, paymentDetails) => {
console.log('Payment successful!', { paymentId, transactionHash });
// Update your order status, redirect to success page, etc.
alert('Payment completed successfully!');
};
// Handle payment errors
const handleError = (error) => {
console.error('Payment error:', error);
alert('Payment failed: ' + error);
};
// Handle modal close
const handleClose = () => {
setIsPaymentOpen(false);
};
return (
<div>
{/* Your checkout form */}
<button onClick={() => setIsPaymentOpen(true)}>
Pay with Crypto
</button>
{/* Coinley Payment Modal */}
<EnhancedSimpleCoinleyPayment
apiKey="your_api_key"
apiSecret="your_api_secret"
apiUrl="https://hub.coinley.io"
config={{
amount: orderTotal,
customerEmail: customerEmail,
merchantName: "Your Store Name",
callbackUrl: `${window.location.origin}/api/webhooks/coinley`,
merchantWalletAddresses: {
polygon: "0xYourPolygonWallet...",
bsc: "0xYourBSCWallet...",
// Add other networks as needed
},
metadata: {
orderId: "ORDER_123",
// Add any custom data
}
}}
onSuccess={handleSuccess}
onError={handleError}
onClose={handleClose}
isOpen={isPaymentOpen}
theme="light"
/>
</div>
);
}
| Property | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Yes | Your Coinley API key |
| apiSecret | string | Yes | Your Coinley API secret |
| apiUrl | string | Yes | Coinley API base URL |
| config.amount | number | Yes | Payment amount in USD |
| config.customerEmail | string | Yes | Customer's email address |
| config.merchantName | string | Yes | Your business name |
| config.merchantWalletAddresses | object | Yes | Your wallet addresses per network |
| isOpen | boolean | Yes | Controls modal visibility |
| theme | string | No | "light" or "dark" (default: "light") |
Add the SDK script to your HTML:
<script src="https://unpkg.com/coinley-pay@latest/dist/coinley-vanilla.min.js"></script>
<script>
// Initialize Coinley
const coinley = new CoinleyVanilla({
apiKey: 'your_api_key',
apiSecret: 'your_api_secret',
apiUrl: 'https://hub.coinley.io',
theme: 'light',
debug: false
});
// Open payment when button is clicked
document.getElementById('payButton').addEventListener('click', function() {
coinley.open(
{
amount: 49.99,
customerEmail: 'customer@example.com',
merchantName: 'Your Store',
merchantWalletAddresses: {
polygon: '0xYourPolygonWallet...',
bsc: '0xYourBSCWallet...'
},
metadata: {
orderId: 'ORDER_123'
}
},
{
onSuccess: function(paymentId, transactionHash, paymentDetails) {
console.log('Payment successful!', paymentId);
alert('Payment completed! Transaction: ' + transactionHash);
},
onError: function(error) {
console.error('Payment failed:', error);
alert('Payment failed: ' + error);
},
onClose: function() {
console.log('Payment modal closed');
}
}
);
});
</script>
You can auto-initialize the SDK using script tag attributes:
<script
src="https://unpkg.com/coinley-pay@latest/dist/coinley-vanilla.min.js"
data-api-key="your_api_key"
data-api-secret="your_api_secret"
data-api-url="https://hub.coinley.io"
data-theme="light"
></script>
<script>
// SDK is already initialized as window.coinley
window.coinley.open({ ... });
</script>
Here's a complete working HTML page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Coinley Payment Example</title>
</head>
<body>
<h1>Buy Product - $49.99</h1>
<input type="email" id="customerEmail" placeholder="Enter your email" />
<button id="payButton">Pay with Crypto</button>
<!-- Include Coinley SDK -->
<script src="https://unpkg.com/coinley-pay@latest/dist/coinley-vanilla.min.js"></script>
<script>
// Initialize Coinley
const coinley = new CoinleyVanilla({
apiKey: 'your_api_key',
apiSecret: 'your_api_secret',
apiUrl: 'https://hub.coinley.io'
});
// Handle payment button click
document.getElementById('payButton').addEventListener('click', () => {
const email = document.getElementById('customerEmail').value;
if (!email) {
alert('Please enter your email');
return;
}
coinley.open(
{
amount: 49.99,
customerEmail: email,
merchantName: 'My Store',
merchantWalletAddresses: {
polygon: '0xYourWallet...'
}
},
{
onSuccess: (paymentId, txHash) => {
alert('Payment successful!\\nTransaction: ' + txHash);
// Redirect to success page or update order
},
onError: (error) => {
alert('Payment failed: ' + error);
}
}
);
});
</script>
</body>
</html>
Get started with the SDK for the fastest crypto payment integration
This section shows how to manually integrate with the Coinley API using Web3.js and custom code.
Looking for a faster option? Check out the SDK Integration above for a 5-minute setup with pre-built UI components.
Sign up at Coinley to get your API key and secret
You don't need blockchain experience
We'll use Web3.js (auto-loaded from CDN) and Axios
<!-- Include required libraries in your HTML -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web3@1.10.0/dist/web3.min.js"></script>
Set up your Coinley API credentials. Keep these secure on your backend.
In production, store API credentials on your backend. The frontend should call your backend API, not Coinley directly.
// Coinley Configuration
const COINLEY_CONFIG = {
baseURL: 'https://hub.coinley.io',
apiKey: 'your_api_key_here',
apiSecret: 'your_api_secret_here'
};
// Store cart and payment state
let cart = [];
let currentPayment = null;
let web3 = null;
let userAccount = null;
let availableNetworks = [];
let networkTokens = {};
Fetch the list of supported blockchain networks and their tokens from Coinley:
async function loadNetworks() {
const response = await axios.get(`${COINLEY_CONFIG.baseURL}/api/networks`, {
headers: {
'X-API-Key': COINLEY_CONFIG.apiKey,
'X-API-Secret': COINLEY_CONFIG.apiSecret
}
});
availableNetworks = response.data.networks.filter(n => !n.isTestnet);
// Sort networks by cost-effectiveness
const sortedNetworks = availableNetworks.sort((a, b) => {
const costRanking = {
'polygon': 1, 'bsc': 2, 'base': 3,
'arbitrum': 4, 'optimism': 5, 'ethereum': 7
};
return (costRanking[a.shortName] || 99) - (costRanking[b.shortName] || 99);
});
// Populate network dropdown
const networkSelect = document.getElementById('paymentNetwork');
networkSelect.innerHTML = sortedNetworks.map(network => {
const costIndicator = network.shortName === 'polygon' || network.shortName === 'bsc'
? ' 💚 (Low fees)' : network.shortName === 'optimism' ? ' ⚠️ (Higher fees)' : '';
return `<option value="${network.shortName}">${network.name}${costIndicator}</option>`;
}).join('');
}
async function loadTokensForNetwork(networkShortName) {
const network = availableNetworks.find(n => n.shortName === networkShortName);
const response = await axios.get(
`${COINLEY_CONFIG.baseURL}/api/networks/${network.id}/tokens`,
{
headers: {
'X-API-Key': COINLEY_CONFIG.apiKey,
'X-API-Secret': COINLEY_CONFIG.apiSecret
}
}
);
networkTokens[networkShortName] = response.data.tokens;
// Populate currency dropdown
const currencySelect = document.getElementById('paymentCurrency');
currencySelect.innerHTML = response.data.tokens.map(token =>
`<option value="${token.symbol}">${token.symbol} - ${token.name}</option>`
).join('');
}
Call the Coinley API to create a payment request. You'll get back smart contract details and fee split configuration.
/api/payments/create
In the SportsPass ticket platform, this function is called when a customer clicks "Connect MetaMask & Pay" with items in their cart. It creates a payment for the total cart value.
async function connectMetaMaskAndPay() {
const email = document.getElementById('customerEmail').value;
const network = document.getElementById('paymentNetwork').value;
const currency = document.getElementById('paymentCurrency').value;
if (!email || cart.length === 0) {
alert('Please enter your email and add items to cart');
return;
}
const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
// Create payment via Coinley API
const response = await axios.post(
`${COINLEY_CONFIG.baseURL}/api/payments/create`,
{
amount: total.toFixed(2), // e.g., "19.99"
currency: currency, // e.g., "USDT"
network: network, // e.g., "polygon"
customerEmail: email,
orderId: `TICKET_${Date.now()}`,
metadata: {
tickets: cart,
platform: 'SportsPass'
}
},
{
headers: {
'X-API-Key': COINLEY_CONFIG.apiKey,
'X-API-Secret': COINLEY_CONFIG.apiSecret,
'Content-Type': 'application/json'
}
}
);
if (response.data.success) {
currentPayment = response.data.payment;
console.log('Payment created:', currentPayment);
// Continue to wallet connection...
}
}
{
"success": true,
"payment": {
"id": "21870d6e-d53d-4e72-8761-e0208a8ceacb",
"amount": "19.99",
"currency": "USDT",
"network": "polygon",
"status": "pending",
"contractAddress": "0x48E967d182F744EdB18AcA7092814f1FE17fcB2d",
"splitterPaymentId": "21870d6e-d53d-4e72-8761-e0208a8ceacb",
"merchantWallet": "0x9d961e093FFeC1577d124Cfe65233fE140E88Fc4",
"coinleyWallet": "0xEe9025Cc02c060C03ba5dba3d19C7ea2e752f44d",
"merchantPercentage": 9700, // 98.5%
"coinleyPercentage": 300, // 1.5%
"expiresAt": "2025-10-15T14:30:00.000Z"
}
}
What's happening: Coinley creates a unique payment ID and tells you which smart contract to use. The contract will automatically split funds 98.5% to you, 1.5% to Coinley.
Connect the user's crypto wallet (MetaMask, Coinbase Wallet, etc.) and switch to the correct blockchain network.
async function connectWallet() {
// Check if user has a Web3 wallet installed
if (typeof window.ethereum === 'undefined') {
alert('Please install MetaMask or another Web3 wallet');
window.open('https://metamask.io/download/', '_blank');
return;
}
// Initialize Web3
web3 = new Web3(window.ethereum);
// Request account access
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
if (!accounts || accounts.length === 0) {
throw new Error('No wallet accounts found');
}
userAccount = accounts[0];
console.log('Connected wallet:', userAccount);
return userAccount;
}
Ensure the customer's wallet is on the correct blockchain network for the payment:
async function switchToNetwork(networkShortName) {
const network = availableNetworks.find(n => n.shortName === networkShortName);
if (!network) throw new Error('Network not found');
// Convert chainId to hex format (required by MetaMask)
const chainIdHex = '0x' + parseInt(network.chainId).toString(16);
try {
// Ask wallet to switch networks
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainIdHex }]
});
console.log(`Switched to ${network.name}`);
} catch (error) {
if (error.code === 4902) {
// Network not added to wallet yet
alert(`Please add ${network.name} to your wallet manually`);
}
throw error;
}
}
User Experience: The wallet will show a popup asking the user to switch networks. This is automatic - you don't need to worry about network configurations.
Always check if the user has enough tokens BEFORE attempting payment. This prevents confusing blockchain errors.
"ERC20: transfer amount exceeds balance" means the user doesn't have enough tokens.
Solution: Check balance first and show a friendly error message before attempting payment.
async function checkTokenBalance(tokenAddress, requiredAmount, decimals) {
// ERC20 standard ABI for balanceOf
const erc20ABI = [{
"constant": true,
"inputs": [{"name": "_owner", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "balance", "type": "uint256"}],
"type": "function"
}];
const tokenContract = new web3.eth.Contract(erc20ABI, tokenAddress);
// Get user's token balance
const balanceWei = await tokenContract.methods
.balanceOf(userAccount)
.call();
// Convert to decimal (e.g., USDT uses 6 decimals)
const balanceDecimal = parseFloat(balanceWei) / Math.pow(10, decimals);
console.log(`Balance: ${balanceDecimal} | Required: ${requiredAmount}`);
// Check if sufficient
if (balanceDecimal < requiredAmount) {
const shortfall = (requiredAmount - balanceDecimal).toFixed(2);
throw new Error(
`Insufficient balance!\n\n` +
`Required: ${requiredAmount}\n` +
`Available: ${balanceDecimal.toFixed(2)}\n` +
`Shortfall: ${shortfall}\n\n` +
`Please add more funds to your wallet.`
);
}
return true;
}
Best Practice: Display the user's current balance in your UI so they know if they need to add funds.
Execute the smart contract payment. This involves two transactions: approve and pay.
This is a security feature of ERC20 tokens - contracts can't take your tokens without explicit permission.
async function executePayment() {
const networkShortName = currentPayment.network;
const currency = currentPayment.currency;
const tokenData = networkTokens[networkShortName].find(t => t.symbol === currency);
const decimals = tokenData.decimals || 6;
const amount = parseFloat(currentPayment.amount);
const amountWei = (amount * Math.pow(10, decimals)).toString();
// ERC20 Token ABI
const erc20ABI = [
{
"constant": false,
"inputs": [
{"name": "_spender", "type": "address"},
{"name": "_value", "type": "uint256"}
],
"name": "approve",
"outputs": [{"name": "", "type": "bool"}],
"type": "function"
},
{
"constant": true,
"inputs": [{"name": "_owner", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "balance", "type": "uint256"}],
"type": "function"
}
];
const tokenContract = new web3.eth.Contract(erc20ABI, tokenData.contractAddress);
// ✅ CRITICAL: Check balance first
const balance = await tokenContract.methods.balanceOf(userAccount).call();
if (BigInt(balance) < BigInt(amountWei)) {
throw new Error('Insufficient token balance');
}
// ✅ STEP 1: Approve token spending
console.log('Requesting token approval...');
const approveTx = await tokenContract.methods
.approve(currentPayment.contractAddress, amountWei)
.send({ from: userAccount });
console.log('Approved! Tx:', approveTx.transactionHash);
// ✅ STEP 2: Execute payment via splitter contract
const splitterABI = [{
"inputs": [{
"components": [
{"name": "token", "type": "address"},
{"name": "amount", "type": "uint256"},
{"name": "paymentId", "type": "string"},
{"name": "recipient1", "type": "address"},
{"name": "recipient2", "type": "address"},
{"name": "recipient3", "type": "address"},
{"name": "recipient1Percentage", "type": "uint256"},
{"name": "recipient2Percentage", "type": "uint256"},
{"name": "recipient3Percentage", "type": "uint256"}
],
"name": "params",
"type": "tuple"
}],
"name": "splitPayment",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}];
const splitterContract = new web3.eth.Contract(
splitterABI,
currentPayment.contractAddress
);
const splitParams = {
token: tokenData.contractAddress,
amount: amountWei,
paymentId: currentPayment.splitterPaymentId,
recipient1: currentPayment.merchantWallet,
recipient2: currentPayment.coinleyWallet,
recipient3: '0x0000000000000000000000000000000000000000',
recipient1Percentage: currentPayment.merchantPercentage,
recipient2Percentage: currentPayment.coinleyPercentage,
recipient3Percentage: 0
};
console.log('Executing payment...');
const paymentTx = await splitterContract.methods
.splitPayment(splitParams)
.send({ from: userAccount });
console.log('Payment successful! Tx:', paymentTx.transactionHash);
// ✅ STEP 3: Verify payment with Coinley
await verifyPayment(paymentTx.transactionHash);
}
What happens: The wallet shows two popups - one to approve tokens, one to execute payment. The smart contract automatically splits funds between merchant (98.5%) and Coinley (1.5%).
Send the transaction hash to Coinley for verification. We check the blockchain to confirm payment.
/api/payments/process
async function verifyPayment(txHash) {
const response = await axios.post(
`${COINLEY_CONFIG.baseURL}/api/payments/process`,
{
paymentId: currentPayment.id,
transactionHash: txHash,
network: currentPayment.network,
senderAddress: userAccount
},
{
headers: {
'X-API-Key': COINLEY_CONFIG.apiKey,
'X-API-Secret': COINLEY_CONFIG.apiSecret,
'Content-Type': 'application/json'
}
}
);
if (!response.data.success) {
throw new Error(response.data.error || 'Verification failed');
}
console.log('Payment verified!', response.data);
}
{
"success": true,
"message": "Payment processed successfully",
"payment": {
"id": "21870d6e-d53d-4e72-8761-e0208a8ceacb",
"status": "completed",
"amount": "19.99",
"transactionHash": "0xabcdef1234567890...",
"senderAddress": "0x581c333..."
},
"onChainData": {
"blockNumber": 18745123,
"gasUsed": "65432",
"confirmed": true
}
}
What Coinley verifies: Transaction exists on blockchain, correct amount was sent, correct recipients received funds, and fee split was executed correctly.
Minimize transaction costs for your users with these strategies (as implemented in SportsPass).
SportsPass displays warnings when users select high-fee networks. Here's how:
// Show fee warning based on selected network
networkSelect.addEventListener('change', (e) => {
const selectedNetwork = e.target.value;
const networkWarning = document.getElementById('networkWarning');
const highFeeNetworks = ['optimism', 'arbitrum', 'ethereum'];
const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
if (highFeeNetworks.includes(selectedNetwork)) {
let estimatedFee = 0;
if (selectedNetwork === 'optimism') {
estimatedFee = 0.61;
networkWarning.innerHTML = `
⚠️ OPTIMISM ALERT: Expected fee ~$${estimatedFee}
(${(estimatedFee / total * 100).toFixed(0)}% of transaction).
Switch to Polygon for ~$0.01 fees.
`;
} else if (selectedNetwork === 'ethereum') {
estimatedFee = 15;
networkWarning.innerHTML = `
🚨 ETHEREUM: Fees typically $10-20.
DO NOT USE for small transactions! Use Polygon instead.
`;
}
networkWarning.classList.remove('hidden');
} else {
networkWarning.classList.add('hidden');
}
});
For Optimism and Arbitrum, SportsPass offers a "direct transfer" option that bypasses the
splitter contract,
reducing fees by 90%. Set isDirectTransfer: true when verifying payment.
Handle common errors gracefully to improve user experience.
User clicked "Reject" in wallet popup
Show: "Payment cancelled. No charges were made."
User doesn't have enough tokens
Show: "Insufficient USDT balance. You need X more USDT."
User is on wrong blockchain network
Show: "Please switch to Polygon network in your wallet."
try {
await connectWallet();
await switchToNetwork('polygon');
await executePayment();
alert('✅ Payment successful!');
} catch (error) {
console.error('Payment error:', error);
// User-friendly error messages
if (error.message.includes('User denied')) {
alert('Payment cancelled. No charges were made.');
} else if (error.message.includes('Insufficient')) {
alert(error.message); // Our custom balance error
} else if (error.code === 4902) {
alert('Please add this network to your wallet.');
} else {
alert('Payment failed: ' + error.message);
}
}
Here's the complete ticket sales platform built with Coinley. This is a real, production-ready implementation.
A complete e-commerce platform for selling sports tickets with cryptocurrency payments. Features include:
See loadNetworks() function - Line 246
Automatically sorts networks by cost-effectiveness and shows visual fee indicators (💚 Low fees, ⚠️ Higher fees)
See network change handler - Line 280
Calculates and displays estimated fees as a percentage of transaction value
See executeDirectTransfer() - Line 537
Reduces Optimism fees by 90% using direct transfer instead of splitter contract
See updateStep() and payment modal - Line
932
Shows users exactly what's happening: approve tokens → execute payment → verify
/api/payments/create
Create a new payment request
{
"amount": "10.00",
"currency": "USDT",
"network": "polygon",
"customerEmail": "user@example.com",
"orderId": "ORDER_123",
"metadata": {
"items": [...],
"platform": "YourApp"
}
}
X-API-Key: your_api_key
X-API-Secret: your_api_secret
Content-Type: application/json
{
"success": true,
"payment": {
"id": "uuid",
"amount": "10.00",
"contractAddress": "0x...",
"merchantWallet": "0x...",
"merchantPercentage": 9700
}
}
/api/payments/process
Verify and complete a payment
{
"paymentId": "uuid",
"transactionHash": "0xabc...",
"network": "polygon",
"senderAddress": "0x123...",
"isDirectTransfer": false
}
{
"success": true,
"message": "Payment processed successfully",
"payment": {
"status": "completed",
"transactionHash": "0xabc..."
}
}
/api/networks
Get list of supported blockchain networks
{
"success": true,
"networks": [
{
"id": "1",
"name": "Polygon",
"shortName": "polygon",
"chainId": "137",
"isTestnet": false
}
]
}
/api/networks/:networkId/tokens
Get supported tokens for a specific network
{
"success": true,
"tokens": [
{
"symbol": "USDT",
"name": "Tether USD",
"contractAddress": "0x...",
"decimals": 6
}
]
}
/api/payments/public/:paymentId
Check payment status publicly (no authentication required)
Public Endpoint: No API credentials needed. Perfect for polling payment status from frontend.
{
"success": true,
"payment": {
"id": "uuid",
"amount": "10.00",
"status": "completed",
"Network": {
"name": "Polygon",
"chainId": "137"
}
}
}