API Documentation

Accept non-custodial stablecoin payments on any chain. Integrate with our REST API in minutes.

Base URL

https://api.payzap.cc

Auth

Bearer <JWT>

Format

JSON

Quick Start

1Sign in at payzap.cc/dashboard with your wallet
2Create a product with a name and price
3Share the payment link or create sessions via API
4Set up webhooks to get notified when payments are confirmed

Authentication

All 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...
GET/v1/auth/nonce

Get 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..."
  }
}
POST/v1/auth

Authenticate with a wallet signature. Returns JWT access + refresh tokens.

Request body

FieldTypeReqDescription
walletAddressstringyesYour wallet address
chainenumyes"evm" | "ton" | "tron" | "solana"
signaturestringyesSigned nonce message
noncestringyesNonce from GET /v1/auth/nonce

Example response

{
  "success": true,
  "data": {
    "accessToken": "eyJhbG...",
    "refreshToken": "eyJhbG...",
    "merchant": { "id": "...", "plan": "free" }
  }
}
POST/v1/auth/refresh

Refresh an expired access token.

Request body

FieldTypeReqDescription
refreshTokenstringyesRefresh token from auth response

Products

Products represent items or services you sell. Each product has a price and generates a payment link.

POST/v1/productsAUTH

Create a new product.

Request body

FieldTypeReqDescription
namestringyesProduct name (1-255 chars)
descriptionstringnoDescription (up to 2000 chars)
priceAmountnumberyesPrice in USD (max 1,000,000). Note: returned as string in API responses
priceCurrencystringnoCurrency code (default: "USD")
acceptedChainsstring[]no"evm" | "ton" | "tron" | "solana" | "binance_pay" | "bybit_pay"
successUrlstringnoURL to redirect customer after successful payment
slugstringnoCustom short-link slug (3-32 chars, lowercase a-z + 0-9 + hyphen). Used at payzap.cc/r/<slug>. Auto-generated if omitted. Globally unique.
metadataobjectnoArbitrary JSON metadata
GET/v1/productsAUTH

List your products.

Query params

ParamTypeDescription
limitnumber1-100 (default: 20)
offsetnumberOffset (default: 0)
GET/v1/products/:idAUTH

Get a single product by ID.

PATCH/v1/products/:idAUTH

Update a product. All fields optional.

Request body

FieldTypeReqDescription
namestringnoNew name
priceAmountnumbernoNew price
activebooleannoEnable/disable
acceptedChainsstring[]noAccepted payment methods
successUrlstring|nullnoSuccess redirect URL (null to clear)
slugstring|nullnoUpdate or remove the short-link slug. Pass null to drop it (slug becomes unbound; old /r/<slug> URL returns 404).
DELETE/v1/products/:idAUTH

Deactivate a product (soft delete).

GET/v1/public/slug/:slug

Resolve 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
  }
}

Payments

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.

POST/v1/payments/session

Create a payment session. Returns a session with wallet address and payment link. Public endpoint — no auth required.

Request body

FieldTypeReqDescription
productIduuidyesProduct ID
chainenumyes"evm" | "ton" | "tron" | "solana" | "binance_pay" | "bybit_pay"
assetenumyes"USDT" | "USDC" | "DAI" | "BUSD"
amountnumbernoOverride product price (dynamic pricing)
customerRefstringnoYour internal customer/order ID
successUrlstringnoOverride product success URL for this session
metadataobjectnoArbitrary 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_..."
  }
}
GET/v1/payments/session/:id

Get session status. Use for polling from your frontend. Public — no auth.

GET/v1/paymentsAUTH

List 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

ParamTypeDescription
limitnumber1-100 (default: 20)
offsetnumberOffset (default: 0)
customerRefstringExternal order ID — exact match. Find a specific session by your own reference.
statusenumpending | confirming | completed | failed | expired | refunded
GET/v1/payments/:idAUTH

Get a single payment by ID.

Refunds

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:

TokenrefundModeMerchant actionConfirm via
EVM USDC / DAIpermitSign returned EIP-712 typed-data/refund/submit
EVM USDT, BSC USDCsponsoredSend ERC-20 transfer (PayZap sponsors gas if balance low)/refund/confirm-transfer
Tron USDT / USDCtron-transferSend TRC-20 transfer (PayZap delegates energy via TronZap → 0 TRX)/refund/confirm-transfer
Solana USDC / USDTsolana-transferSign 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.

POST/v1/payments/:id/refundAUTH

Initiate 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..."
  }
}
POST/v1/payments/:id/refund/submitAUTH

Submit 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

FieldTypeReqDescription
refundIduuidyesRefund ID from /refund initiation
signaturehex stringyesMerchant's EIP-712 signature
POST/v1/payments/:id/refund/confirm-transferAUTH

Confirm 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

FieldTypeReqDescription
refundIduuidyesRefund ID
txHashstringyesOn-chain tx hash (EVM 0x..., Tron hex, Solana base58 signature)
GET/v1/payments/:id/refundsAUTH

List all refunds for a payment session, including past attempts (cancelled, failed) for audit.

GET/v1/payments/:id/refund/:refundIdAUTH

Get a single refund by ID with current status (pending, submitted, confirmed, failed, cancelled).

Gasless Payments

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 & TokenMechanismEndpoint
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 USDCSponsored (bridged USDC has no permit)/sponsor-gas
TRON USDT / USDCEnergy + bandwidth rented via TronZap and delegated to buyer/delegate-energy
Solana USDC / USDTFacilitator 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).

POST/v1/public/session/:id/permit-data

Build 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

FieldTypeReqDescription
buyeraddressyesBuyer wallet address (0x…)
POST/v1/public/session/:id/permit

Submit a signed EIP-2612 permit. Backend enqueues `permit() + transferFrom()` for on-chain settlement. Public — no auth.

Request body

FieldTypeReqDescription
owneraddressyesBuyer address (must match the signer)
signaturehex stringyesEIP-712 signature
POST/v1/public/session/:id/sponsor-gas

Send 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

FieldTypeReqDescription
buyerAddressaddressyesBuyer wallet address
POST/v1/public/session/:id/delegate-energy

Rent ~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

FieldTypeReqDescription
buyerAddressstringyesBuyer TRON address (T…)
GET/v1/public/tron-energy-estimate

Quote-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

ParamTypeDescription
buyerAddressstringBuyer TRON address
merchantWalletstringMerchant TRON address
POST/v1/public/session/:id/solana/sponsor-tx

Build 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

FieldTypeReqDescription
senderAddressstringyesBuyer Solana address (base58)

Example response

{
  "success": true,
  "data": {
    "serializedTx": "base64...",
    "feePayer": "Ba3jY..."
  }
}

Every product gets a payment link at https://payzap.cc/pay/<product_id>. Customize the checkout experience with query parameters:

ParamTypeDescription
success_urlstringRedirect URL after successful payment (overrides product setting)
amountnumberOverride product price (dynamic pricing)
refstringCustomer reference / order ID
themestringUI 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.

Embed Widget

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.

Custom Checkout

Build your own payment UI using the PayZap API. Two endpoints are all you need.

Flow

1Create a payment session via POST /v1/payments/session — you get the merchantWallet, amount, and asset
2User sends tokens to merchantWallet using your UI (wagmi, ethers, viem, TonConnect, etc.)
3Poll 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

StatusDescription
pendingWaiting for payment
confirmingTransaction detected, waiting for block confirmations
completedPayment confirmed on-chain
expiredSession timed out (30 min)
failedTransaction 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.

Webhooks

Get real-time notifications when payment events occur. Webhooks are signed with HMAC-SHA256 for verification.

Events

EventDescription
payment.pendingTransfer detected on-chain, waiting for required confirmations
payment.completedPayment fully confirmed on-chain
payment.failedPayment failed or was rejected
payment.expiredSession expired without payment
refund.completedRefund settled on-chain (any mode: EVM permit/sponsored, Tron, Solana). Payload includes refund_id, refund_amount, refund_tx_hash.
refund.failedRefund 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');
}
POST/v1/webhooksAUTH

Create a webhook endpoint. The secret is returned only once — store it securely.

Request body

FieldTypeReqDescription
urlstringyesHTTPS endpoint URL
eventsstring[]noEvent filter (default: all events)

Example response

{
  "success": true,
  "data": {
    "id": "whk_...",
    "url": "https://example.com/webhook",
    "events": ["payment.completed", "payment.failed"],
    "secret": "whsec_..."
  }
}
GET/v1/webhooksAUTH

List your webhook endpoints.

PATCH/v1/webhooks/:idAUTH

Update a webhook URL or event filter.

DELETE/v1/webhooks/:idAUTH

Delete a webhook endpoint.

POST/v1/webhooks/:id/testAUTH

Send a test webhook event to verify your endpoint.

Merchant

Manage your merchant profile, wallets, and API keys.

GET/v1/merchantAUTH

Get your merchant profile, wallets, and usage.

GET/v1/merchant/walletsAUTH

List your connected wallets.

POST/v1/merchant/walletsAUTH

Add a new wallet address.

Request body

FieldTypeReqDescription
chainenumyes"evm" | "ton" | "tron" | "solana"
addressstringyesWallet address
DELETE/v1/merchant/wallets/:idAUTH

Remove a wallet.

POST/v1/merchant/api-keysAUTH

Create an API key. The raw key is returned only once.

Request body

FieldTypeReqDescription
namestringnoLabel for this key
GET/v1/merchant/api-keysAUTH

List API keys (without secret values).

DELETE/v1/merchant/api-keys/:idAUTH

Revoke an API key.

Payment Config

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.

GET/v1/merchant/payment-configAUTH

Get your enabled payment methods and EVM networks.

Example response

{
  "success": true,
  "data": {
    "enabledMethods": ["evm", "ton", "binance_pay"],
    "evmNetworks": ["ethereum", "base", "arbitrum"]
  }
}
PUT/v1/merchant/payment-configAUTH

Update enabled payment methods and EVM networks.

Request body

FieldTypeReqDescription
enabledMethodsstring[]yes"evm" | "ton" | "tron" | "solana" | "binance_pay" | "bybit_pay"
evmNetworksstring[]no"ethereum" | "base" | "arbitrum" | "polygon" | "bsc" | "optimism"

Exchange Credentials

Connect Binance Pay or Bybit Pay to accept payments via exchange checkout. Customers pay using their exchange app.

GET/v1/merchant/exchange-credentialsAUTH

List your exchange API credentials (secrets are masked).

Example response

{
  "success": true,
  "data": [
    {
      "id": "...",
      "provider": "binance_pay",
      "apiKey": "abc***xyz",
      "merchantIdExt": "123456",
      "active": true
    }
  ]
}
POST/v1/merchant/exchange-credentialsAUTH

Add or update exchange API credentials. Required to accept Binance Pay or Bybit Pay.

Request body

FieldTypeReqDescription
providerenumyes"binance_pay" | "bybit_pay"
apiKeystringyesExchange API key
apiSecretstringyesExchange API secret
merchantIdExtstringnoBinance merchant ID (required for Binance Pay)
DELETE/v1/merchant/exchange-credentials/:providerAUTH

Remove exchange credentials. Provider: "binance_pay" or "bybit_pay".

Errors

All errors follow a consistent format. HTTP status codes are used meaningfully.

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Product not found"
  }
}
StatusCodeMeaning
400VALIDATION_ERRORInvalid request body or params
401UNAUTHORIZEDMissing or invalid JWT
404NOT_FOUNDResource does not exist
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORServer error

Looking for x402 protocol docs?

Integrate AI agent payments via the HTTP 402 protocol.

x402 Documentation