Accept non-custodial stablecoin payments on any chain. Integrate with our REST API in minutes.
Base URL
https://api.payzap.ccAuth
Bearer <JWT>Format
JSONAll authenticated endpoints require a JWT in the Authorization header. Get a token by signing a nonce with your wallet.
Auth header format
Authorization: Bearer eyJhbGciOiJIUzI1NiIs.../v1/auth/nonceGet a one-time nonce for wallet signature authentication. Nonces expire after 5 minutes.
Example response
{
"success": true,
"data": {
"nonce": "a1b2c3d4...",
"message": "Sign in to PayZap\n\nNonce: a1b2c3d4..."
}
}/v1/authAuthenticate with a wallet signature. Returns JWT access + refresh tokens.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| walletAddress | string | yes | Your wallet address |
| chain | enum | yes | "evm" | "ton" | "tron" | "solana" |
| signature | string | yes | Signed nonce message |
| nonce | string | yes | Nonce from GET /v1/auth/nonce |
Example response
{
"success": true,
"data": {
"accessToken": "eyJhbG...",
"refreshToken": "eyJhbG...",
"merchant": { "id": "...", "plan": "free" }
}
}/v1/auth/refreshRefresh an expired access token.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| refreshToken | string | yes | Refresh token from auth response |
Products represent items or services you sell. Each product has a price and generates a payment link.
/v1/productsAUTHCreate a new product.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| name | string | yes | Product name (1-255 chars) |
| description | string | no | Description (up to 2000 chars) |
| priceAmount | number | yes | Price in USD (max 1,000,000). Note: returned as string in API responses |
| priceCurrency | string | no | Currency code (default: "USD") |
| acceptedChains | string[] | no | "evm" | "ton" | "tron" | "solana" | "binance_pay" | "bybit_pay" |
| successUrl | string | no | URL to redirect customer after successful payment |
| slug | string | no | Custom short-link slug (3-32 chars, lowercase a-z + 0-9 + hyphen). Used at payzap.cc/r/<slug>. Auto-generated if omitted. Globally unique. |
| metadata | object | no | Arbitrary JSON metadata |
/v1/productsAUTHList your products.
Query params
| Param | Type | Description |
|---|---|---|
| limit | number | 1-100 (default: 20) |
| offset | number | Offset (default: 0) |
/v1/products/:idAUTHGet a single product by ID.
/v1/products/:idAUTHUpdate a product. All fields optional.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| name | string | no | New name |
| priceAmount | number | no | New price |
| active | boolean | no | Enable/disable |
| acceptedChains | string[] | no | Accepted payment methods |
| successUrl | string|null | no | Success redirect URL (null to clear) |
| slug | string|null | no | Update or remove the short-link slug. Pass null to drop it (slug becomes unbound; old /r/<slug> URL returns 404). |
/v1/products/:idAUTHDeactivate a product (soft delete).
/v1/public/slug/:slugResolve a short-link slug to its productId. Public — no auth. Atomically increments the slug click counter (used for influencer / channel attribution). The /r/<slug> page on payzap.cc calls this internally before redirecting to /pay/<productId>; merchants typically don't hit this directly except for analytics tooling.
Example response
{
"success": true,
"data": {
"productId": "13463560-8a8b-44e1-8216-63c2bf205a43",
"slug": "coffee",
"clicks": 1247
}
}Payment sessions are created when a customer initiates checkout. The session tracks the payment lifecycle from pending to confirmed.
Dynamic pricing: Pass the amount field when creating a session to override the product price. Useful for marketplaces, carts, and pay-what-you-want.
/v1/payments/sessionCreate a payment session. Returns a session with wallet address and payment link. Public endpoint — no auth required.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| productId | uuid | yes | Product ID |
| chain | enum | yes | "evm" | "ton" | "tron" | "solana" | "binance_pay" | "bybit_pay" |
| asset | enum | yes | "USDT" | "USDC" | "DAI" | "BUSD" |
| amount | number | no | Override product price (dynamic pricing) |
| customerRef | string | no | Your internal customer/order ID |
| successUrl | string | no | Override product success URL for this session |
| metadata | object | no | Arbitrary JSON metadata |
Example response
{
"success": true,
"data": {
"id": "sess_...",
"productId": "...",
"merchantWallet": "0x...",
"amount": "49.00",
"asset": "USDT",
"chain": "evm",
"status": "pending",
"expiresAt": "2026-03-17T12:30:00Z",
"paymentUrl": "https://payzap.cc/pay/prod_..."
}
}/v1/payments/session/:idGet session status. Use for polling from your frontend. Public — no auth.
/v1/paymentsAUTHList your payment sessions. Supports filtering by customerRef (the external order ID you passed at session creation — exact match) and status. Useful when an upstream system like a taxi backend needs to round-trip "what is the state of order_42?" without keeping its own session-id mapping.
Query params
| Param | Type | Description |
|---|---|---|
| limit | number | 1-100 (default: 20) |
| offset | number | Offset (default: 0) |
| customerRef | string | External order ID — exact match. Find a specific session by your own reference. |
| status | enum | pending | confirming | completed | failed | expired | refunded |
/v1/payments/:idAUTHGet a single payment by ID.
Refund a completed payment back to the buyer. Cross-chain — every chain we support for buy-side payments supports refunds with the same gasless ergonomics as the buy flow.
Flow: initiate via POST /refund; the response carries a refundMode discriminator with one of four values, each with its own follow-up endpoint:
| Token | refundMode | Merchant action | Confirm via |
|---|---|---|---|
| EVM USDC / DAI | permit | Sign returned EIP-712 typed-data | /refund/submit |
| EVM USDT, BSC USDC | sponsored | Send ERC-20 transfer (PayZap sponsors gas if balance low) | /refund/confirm-transfer |
| Tron USDT / USDC | tron-transfer | Send TRC-20 transfer (PayZap delegates energy via TronZap → 0 TRX) | /refund/confirm-transfer |
| Solana USDC / USDT | solana-transfer | Sign pre-built fee_payer tx (PayZap covers SOL fee) | /refund/confirm-transfer |
All flows fire refund.completed webhook on success or refund.failed on permanent failure (with refund_reason populated). Backend verifies amount delta with 0.1% tolerance regardless of chain.
/v1/payments/:id/refundAUTHInitiate a refund for a completed payment. Returns a discriminated union keyed on `refundMode`. Cross-chain — works for EVM (permit + sponsored), Tron, and Solana. The shape of the response tells the merchant exactly what to do next: sign typed-data (permit), build an ERC-20 transfer (sponsored), build a TRC-20 transfer (tron), or sign a pre-built fee_payer transaction (solana).
Example response
// EVM permit-mode (USDC / DAI / ERC-3009)
{
"success": true,
"data": {
"refundId": "rfnd_...",
"amount": 49,
"asset": "USDC",
"buyerWallet": "0x...",
"refundMode": "permit",
"permitData": {
"domain": { "name": "USD Coin", "version": "2", "chainId": 8453, "verifyingContract": "0x..." },
"types": { "Permit": [...] },
"primaryType": "Permit",
"message": { "owner": "0x...", "spender": "0x...", "value": "49000000", "nonce": "0", "deadline": "..." }
}
}
}
// EVM sponsored-mode (USDT, BSC USDC, etc — non-permit tokens)
{
"success": true,
"data": {
"refundId": "rfnd_...",
"amount": 49,
"asset": "USDT",
"buyerWallet": "0x...",
"refundMode": "sponsored",
"merchantWallet": "0x...",
"tokenAddress": "0x...",
"chainId": 8453,
"rawAmount": "49000000",
"gasSponsored": true,
"gasSponsorTxHash": "0x..."
}
}
// Tron-mode (USDT / USDC TRC-20)
// energyDelegated=true means PayZap rented ~65k energy + bandwidth via
// TronZap and delegated it to the merchant address for 1 hour, so the
// refund broadcast costs the merchant 0 TRX (PayZap absorbs ~$1).
{
"success": true,
"data": {
"refundId": "rfnd_...",
"amount": 49,
"asset": "USDT",
"buyerWallet": "T...",
"refundMode": "tron-transfer",
"merchantWallet": "T...",
"tokenAddress": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"rawAmount": "49000000",
"energyDelegated": true,
"energyAmount": 64285,
"energyTransactionId": "01kqq..."
}
}
// Solana-mode (USDC / USDT SPL)
// serializedTx, when non-null, is base64 of a transaction with feePayer
// set to PayZap's facilitator and partial-signature already applied.
// Merchant adds their token-authority signature in their wallet and
// submits — pays 0 SOL. If serializedTx is null, the facilitator was
// unavailable; merchant builds + sends the SPL transfer themselves
// (paying ~$0.0004 in SOL).
{
"success": true,
"data": {
"refundId": "rfnd_...",
"amount": 49,
"asset": "USDC",
"buyerWallet": "Buyer11...",
"refundMode": "solana-transfer",
"merchantWallet": "Merch11...",
"mintAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"rawAmount": "49000000",
"decimals": 6,
"serializedTx": "AQA...base64...",
"feePayer": "FacilitatorAddr11..."
}
}/v1/payments/:id/refund/submitAUTHSubmit a signed permit for a refund (EVM permit-mode only — USDC, DAI, ERC-3009 tokens). Enqueues `permit() + transferFrom(merchant → buyer)` via the x402 settlement queue. Returns immediately; the refund.completed webhook fires when the on-chain settlement confirms.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| refundId | uuid | yes | Refund ID from /refund initiation |
| signature | hex string | yes | Merchant's EIP-712 signature |
/v1/payments/:id/refund/confirm-transferAUTHConfirm a refund where the merchant has already broadcast the on-chain transfer. Multi-chain — backend dispatches based on the refund's chain. EVM: 0x-prefixed 64-hex tx hash; backend reads receipt + verifies the Transfer event recipient + amount within 0.1%. Tron: 64-char hex txID (with or without 0x); backend hits TronGrid getTransactionInfoByID and matches the TRC-20 Transfer event. Solana: base58 transaction signature; backend reads getTransaction.meta.postTokenBalances delta on the buyer's ATA and requires it == expected raw amount within 0.1%. Each chain enforces a 0.1% tolerance on the amount delta to catch wrong-amount refunds; rejects fire `refund.failed` webhook.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| refundId | uuid | yes | Refund ID |
| txHash | string | yes | On-chain tx hash (EVM 0x..., Tron hex, Solana base58 signature) |
/v1/payments/:id/refundsAUTHList all refunds for a payment session, including past attempts (cancelled, failed) for audit.
/v1/payments/:id/refund/:refundIdAUTHGet a single refund by ID with current status (pending, submitted, confirmed, failed, cancelled).
PayZap covers buyer gas across multiple chains so they can pay without holding native tokens. The widget calls these endpoints automatically — they're documented here for custom checkouts.
| Chain & Token | Mechanism | Endpoint |
|---|---|---|
| EVM L2 USDC Base, Arbitrum, Polygon | ERC-2612 permit, facilitator submits permit() + transferFrom() | /permit-data → /permit |
| EVM USDT Base, Arbitrum, Polygon, BSC | Facilitator sends gas to buyer, buyer signs a normal ERC-20 transfer | /sponsor-gas |
| BSC USDC | Sponsored (bridged USDC has no permit) | /sponsor-gas |
| TRON USDT / USDC | Energy + bandwidth rented via TronZap and delegated to buyer | /delegate-energy |
| Solana USDC / USDT | Facilitator co-signs as fee_payer; wallet adds buyer signature and broadcasts | /solana/sponsor-tx |
Enable per-merchant: set gasless.enabled = true via the payment-config endpoint or the dashboard. Per-merchant fee-handling modes: absorb (merchant pays gas), passthrough (default — added to buyer's total), fixed (flat fee).
/v1/public/session/:id/permit-dataBuild EIP-2612 permit data for a gasless USDC/DAI payment. Buyer's wallet signs the returned typed-data; the signed permit is then submitted via /permit. Public — no auth.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| buyer | address | yes | Buyer wallet address (0x…) |
/v1/public/session/:id/permitSubmit a signed EIP-2612 permit. Backend enqueues `permit() + transferFrom()` for on-chain settlement. Public — no auth.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| owner | address | yes | Buyer address (must match the signer) |
| signature | hex string | yes | EIP-712 signature |
/v1/public/session/:id/sponsor-gasSend a small amount of native gas (ETH/MATIC/BNB) to the buyer so they can broadcast a normal ERC-20 transfer. Used for USDT and BSC USDC where there's no permit. Public — no auth, rate-limited.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| buyerAddress | address | yes | Buyer wallet address |
/v1/public/session/:id/delegate-energyRent ~65k Energy + ~1.5k Bandwidth from TronZap and delegate to the buyer's TRON address so a TRC-20 USDT transfer costs 0 TRX. Idempotent. Public — no auth, rate-limited.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| buyerAddress | string | yes | Buyer TRON address (T…) |
/v1/public/tron-energy-estimateQuote-only — returns the estimated Energy + cost for delegating to a buyer's TRON address. Used by the widget to show gas-fee badges. Public.
Query params
| Param | Type | Description |
|---|---|---|
| buyerAddress | string | Buyer TRON address |
| merchantWallet | string | Merchant TRON address |
/v1/public/session/:id/solana/sponsor-txBuild an SPL transfer with feePayer set to PayZap's Solana facilitator and pre-sign as fee_payer. Returns the base64 transaction; the buyer's wallet adds their signature and broadcasts. Buyer pays 0 SOL. Public — no auth.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| senderAddress | string | yes | Buyer Solana address (base58) |
Example response
{
"success": true,
"data": {
"serializedTx": "base64...",
"feePayer": "Ba3jY..."
}
}Share a hosted payment page with your customers. Supports query parameters for customization.
Every product gets a payment link at https://payzap.cc/pay/<product_id>. Customize the checkout experience with query parameters:
| Param | Type | Description |
|---|---|---|
| success_url | string | Redirect URL after successful payment (overrides product setting) |
| amount | number | Override product price (dynamic pricing) |
| ref | string | Customer reference / order ID |
| theme | string | UI theme: "dark" (default) or "light" |
Example
https://payzap.cc/pay/prod_abc123?success_url=https://myshop.com/thanks&ref=order_456
Success redirect: After payment, the customer is automatically redirected to the success URL with query parameters: session_id, tx_hash, amount, asset, status.
Add a payment button to any website with a single script tag. No framework required.
Include the widget script and add a button with a data-payzap attribute pointing to your product ID:
<script src="https://payzap.cc/v1.js"></script> <button data-payzap="prod_abc123"> Pay $49.00 </button>
JavaScript API
For programmatic control, use PayZap.open():
PayZap.open({
productId: 'prod_abc123',
amount: 99.00, // optional: override price
ref: 'order_789', // optional: your internal reference
theme: 'dark', // optional: 'dark' | 'light'
onSuccess: (data) => {
console.log('Paid!', data.sessionId, data.txHash);
},
onClose: () => {
console.log('Widget closed');
}
});Note: The API returns priceAmount as a string (e.g. "49.00") due to PostgreSQL numeric precision. Always use parseFloat() or Number() before arithmetic, and Intl.NumberFormat for display formatting.
Build your own payment UI using the PayZap API. Two endpoints are all you need.
Flow
POST /v1/payments/session — you get the merchantWallet, amount, and assetmerchantWallet using your UI (wagmi, ethers, viem, TonConnect, etc.)GET /v1/payments/session/:id until status is "completed" (or set up a webhook)Step 1 — Create session
const res = await fetch('https://api.payzap.cc/v1/payments/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId: 'prod_abc123',
chain: 'evm',
asset: 'USDT',
network: 'base', // optional: specific EVM network
amount: 49.00, // optional: override product price
customerRef: 'order_789', // optional: your internal reference
}),
});
const { data: session } = await res.json();
// session.id — session ID (for polling)
// session.merchantWallet — send tokens here
// session.amount — amount to send (string, e.g. "49.00")
// session.asset — token symbol (e.g. "USDT")
// session.expiresAt — session expires (30 min)Step 2 — Send tokens (EVM example with viem)
import { parseUnits } from 'viem';
// ERC-20 transfer to merchant wallet
const tx = await walletClient.writeContract({
address: USDT_CONTRACT, // token contract on chosen network
abi: [{
name: 'transfer',
type: 'function',
inputs: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ type: 'bool' }],
}],
functionName: 'transfer',
args: [
session.merchantWallet,
parseUnits(session.amount, 6), // 6 decimals for USDT/USDC
],
});Step 3 — Poll for confirmation
async function waitForPayment(sessionId) {
while (true) {
const res = await fetch(
`https://api.payzap.cc/v1/payments/session/${sessionId}`
);
const { data } = await res.json();
if (data.status === 'completed') {
return { txHash: data.txHash, explorerUrl: data.txExplorerUrl };
}
if (data.status === 'expired' || data.status === 'failed') {
throw new Error(`Payment ${data.status}`);
}
await new Promise(r => setTimeout(r, 3000)); // poll every 3s
}
}Session statuses
| Status | Description |
|---|---|
| pending | Waiting for payment |
| confirming | Transaction detected, waiting for block confirmations |
| completed | Payment confirmed on-chain |
| expired | Session timed out (30 min) |
| failed | Transaction failed or reverted |
Important: amount is returned as a string (e.g. "49.00"). Use parseFloat() for arithmetic and Intl.NumberFormat for display.
Tip: You don't need auth to create sessions or poll status. These are public endpoints — safe to call from the browser. Use webhooks for server-side confirmation.
Get real-time notifications when payment events occur. Webhooks are signed with HMAC-SHA256 for verification.
Events
| Event | Description |
|---|---|
| payment.pending | Transfer detected on-chain, waiting for required confirmations |
| payment.completed | Payment fully confirmed on-chain |
| payment.failed | Payment failed or was rejected |
| payment.expired | Session expired without payment |
| refund.completed | Refund settled on-chain (any mode: EVM permit/sponsored, Tron, Solana). Payload includes refund_id, refund_amount, refund_tx_hash. |
| refund.failed | Refund settlement failed permanently (verification mismatch, on-chain revert, etc). Payload includes refund_reason populated from the chain-side check. |
Verifying webhook signatures
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(body))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your handler:
const sig = req.headers['x-payzap-signature'];
if (!verifyWebhook(req.body, sig, 'whsec_...')) {
return res.status(401).send('Invalid signature');
}/v1/webhooksAUTHCreate a webhook endpoint. The secret is returned only once — store it securely.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| url | string | yes | HTTPS endpoint URL |
| events | string[] | no | Event filter (default: all events) |
Example response
{
"success": true,
"data": {
"id": "whk_...",
"url": "https://example.com/webhook",
"events": ["payment.completed", "payment.failed"],
"secret": "whsec_..."
}
}/v1/webhooksAUTHList your webhook endpoints.
/v1/webhooks/:idAUTHUpdate a webhook URL or event filter.
/v1/webhooks/:idAUTHDelete a webhook endpoint.
/v1/webhooks/:id/testAUTHSend a test webhook event to verify your endpoint.
Manage your merchant profile, wallets, and API keys.
/v1/merchantAUTHGet your merchant profile, wallets, and usage.
/v1/merchant/walletsAUTHList your connected wallets.
/v1/merchant/walletsAUTHAdd a new wallet address.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| chain | enum | yes | "evm" | "ton" | "tron" | "solana" |
| address | string | yes | Wallet address |
/v1/merchant/wallets/:idAUTHRemove a wallet.
/v1/merchant/api-keysAUTHCreate an API key. The raw key is returned only once.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| name | string | no | Label for this key |
/v1/merchant/api-keysAUTHList API keys (without secret values).
/v1/merchant/api-keys/:idAUTHRevoke an API key.
Configure which payment methods and EVM networks are available on your checkout pages.
Available methods: evm, ton, tron, solana, binance_pay, bybit_pay. Exchange pay methods require configured exchange credentials.
/v1/merchant/payment-configAUTHGet your enabled payment methods and EVM networks.
Example response
{
"success": true,
"data": {
"enabledMethods": ["evm", "ton", "binance_pay"],
"evmNetworks": ["ethereum", "base", "arbitrum"]
}
}/v1/merchant/payment-configAUTHUpdate enabled payment methods and EVM networks.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| enabledMethods | string[] | yes | "evm" | "ton" | "tron" | "solana" | "binance_pay" | "bybit_pay" |
| evmNetworks | string[] | no | "ethereum" | "base" | "arbitrum" | "polygon" | "bsc" | "optimism" |
Connect Binance Pay or Bybit Pay to accept payments via exchange checkout. Customers pay using their exchange app.
/v1/merchant/exchange-credentialsAUTHList your exchange API credentials (secrets are masked).
Example response
{
"success": true,
"data": [
{
"id": "...",
"provider": "binance_pay",
"apiKey": "abc***xyz",
"merchantIdExt": "123456",
"active": true
}
]
}/v1/merchant/exchange-credentialsAUTHAdd or update exchange API credentials. Required to accept Binance Pay or Bybit Pay.
Request body
| Field | Type | Req | Description |
|---|---|---|---|
| provider | enum | yes | "binance_pay" | "bybit_pay" |
| apiKey | string | yes | Exchange API key |
| apiSecret | string | yes | Exchange API secret |
| merchantIdExt | string | no | Binance merchant ID (required for Binance Pay) |
/v1/merchant/exchange-credentials/:providerAUTHRemove exchange credentials. Provider: "binance_pay" or "bybit_pay".
All errors follow a consistent format. HTTP status codes are used meaningfully.
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Product not found"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request body or params |
| 401 | UNAUTHORIZED | Missing or invalid JWT |
| 404 | NOT_FOUND | Resource does not exist |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Server error |
Integrate AI agent payments via the HTTP 402 protocol.
x402 Documentation