UTOPAYd Docs api.utopay.app RU ← Back to site Sign in
Documentation · API v1

Accept card payments, receive USDC — with or without an account

Your customer pays in their own currency — card, PayPal, Revolut, Apple Pay, bank transfer. A payment provider converts it to USDC on Polygon, and the USDC is split on-chain: your share goes to your wallet (or to your UTOPAY balance), our 4.5% fee to us. You can send the buyer to our payment page, or straight to one payment method.

How a payment works

The flow is the same in every mode. Only the destination of the money and the tooling around it differ.

CreateYou call the API or share a payment link. We return a checkout_url.
PayThe buyer opens it: either our payment page with all methods, or — if you set provider — straight the method you chose.
ConvertThe provider charges the buyer in currency and sends USDC (Polygon) to a one-time address.
SplitThe one-time address forwards the USDC on-chain: your share, our fee, referral shares if any.
ConfirmThe order becomes paid. With an account you also get a webhook and a Telegram message.

Base URL for every API call: https://api.utopay.app. Requests and responses are JSON (content-type: application/json). Amounts are numbers in the order's currency; times are Unix milliseconds.

Quick start

A real payment in one request — no sign-up. The buyer is sent straight to PayPal; if PayPal can't take this payment, our payment page opens with the other methods.

cURL
curl -X POST https://api.utopay.app/v1/pay \
  -H "content-type: application/json" \
  -d '{
    "amount": 100,
    "currency": "USD",
    "payout_wallet": "0xYourPolygonWalletAddress000000000000000000",
    "provider": "paypal",
    "success_url": "https://yoursite.com/thanks"
  }'
# → { "id": "ord_…", "checkout_url": "https://api.utopay.app/pay/ord_…/go?provider=paypal", … }

Redirect the buyer to checkout_url. About 95.5 USDC arrives in your wallet after the payment. With an account you would call POST /v1/payments with an API key instead — same fields, plus history, webhooks and the balance.

Compare the modes

No account Account · Direct Account · Stored
EndpointPOST /v1/payPOST /v1/payments
AuthenticationnoneBearer utopay_sk_…
You receiveUSDC (Polygon) in the wallet from the requestUSDC (Polygon) in the project's walletBalance in the order's currency
Send buyer straight to a method (provider)✓✓✓
All currencies✓✓✓
Dashboard & order history—✓✓
Webhooks / Telegram alerts—✓✓
Payment links from the dashboard—✓✓
Test mode (sandbox)—✓✓
Balance, exchange, swap, withdrawal——✓
Referral program—✓✓
Fee4.5%4.5%

Without an account

POST /v1/pay

No Authorization header. The money always goes directly to payout_wallet — an EVM address that can receive USDC on Polygon. Same body as POST /v1/payments, plus:

FieldTypeRequiredNotes
payout_walletstring✓0x + 40 hex characters. Where your share is sent.
  • Always direct: "settlement": "custody" returns 400 — there is no balance to credit without an account.
  • Flat 4.5% fee, no referral program, no webhooks (callback_url is stored but not called), no test mode, standard checkout design.
  • Tracking: poll the public GET /api/checkout/:id/status (no key), or watch your wallet.
Double-check the wallet. The split is performed on-chain and cannot be reversed. A typo or a non-Polygon address means lost funds.

Account & API keys

Sign upOn utopay.app/login: email, password (8+ chars, upper & lower case, a digit) and your Telegram @username.
Confirm in TelegramPress Start in our bot from that same Telegram account. It also enables payment alerts and password recovery.
Funds modeFunds & payouts: connect a USDC wallet (direct) or keep funds on UTOPAY.
Create a keyAPI keys: a test key first, a live key when ready.

An account holds up to 20 projects (one per site). Each has its own keys, webhooks, wallet, branding, funds mode and stats; the balance is shared across the account.

Header
Authorization: Bearer utopay_sk_live_xxxxxxxxxxxxxxxxxxxx
KeyFormatUse
Live secretutopay_sk_live_…Server side only. Creates real payments.
Test secretutopay_sk_test_…Simulated payments, no money moves — Test mode.
Publishableutopay_pk_…Cannot create payments (403). Reserved for client-side use.

A key is shown once; we store only its hash. Never put a secret key in browser or mobile code.

Create a payment

POST /v1/payments Bearer sk

FieldTypeReq.Notes
amountnumber✓> 0, in currency, rounded to 2 decimals. Max 1 000 000.
currencystring—ISO code the buyer pays in, default USD. Case-insensitive. See Currencies.
providerstring—Send the buyer straight to this payment method, skipping our page — e.g. paypal, revolut, stripe. Falls back to our page if unavailable. See Send buyer to a method. Alias: payment_option (wins if both are sent).
settlementstring—"direct" or "custody". Default: the project's funds mode.
payment_methodsstring—What the buyer sees on the payment page: "both" (card & wallets + cryptocurrency tabs), "cards" or "crypto". Default: the project setting (Funds & payouts), otherwise "both". With crypto the buyer picks a coin and pays 1.5%; funds go straight to your crypto wallets and the order gets paid_with: "crypto". custody orders accept cards only.
emailstring—Buyer's email (validated), passed to the provider.
descriptionstring—Up to 500 chars. Shown to the buyer, returned in webhooks.
success_urlstring—Absolute http(s) URL: where the provider returns the buyer and the "Return to site" button after payment.
cancel_urlstring—Absolute http(s) URL, passed to the provider for a cancelled payment.
callback_urlstring—Absolute http(s) URL. Every event of this order (payment.succeeded, failed, underpaid, expired) is POSTed there in the webhook format, signed with x-utopay-signature whose HMAC key is sha256(secret key) (hex) of the key that created the order. Used by the CMS plugins. Payments with an API key only.
cURL
curl -X POST https://api.utopay.app/v1/payments \
  -H "Authorization: Bearer utopay_sk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{
    "amount": 250,
    "currency": "EUR",
    "email": "[email protected]",
    "description": "Invoice #118",
    "provider": "revolut",
    "success_url": "https://yoursite.com/thanks"
  }'
Response
{
  "id": "ord_Kd83mQp0aVxY",
  "status": "pending",
  "amount": 250, "currency": "EUR",
  "checkout_url": "https://api.utopay.app/pay/ord_Kd83mQp0aVxY/go?provider=revolut",
  "payment_page_url": "https://api.utopay.app/pay/ord_Kd83mQp0aVxY",
  "provider": "revolut",
  "routing": { "mode": "direct_to_provider", "requested_provider": "revolut", "reason": null },
  "settlement": "direct",
  "payout_coin": "USDC", "payout_network": "polygon",
  …  full list in Order object
}

Always redirect the buyer to checkout_url. Keep payment_page_url if you want a "choose another method" link — it always opens our page.

Send the buyer straight to a payment method

Works in every mode — with an API key, without an account, in dashboard payment links and in the no-code form on /payment-links. Set provider and the buyer goes directly to that provider (e.g. PayPal) without seeing our payment form.

At creationWe check the provider. Unknown id → 400 with the list of valid ids. Known but unavailable now (offline, amount below its minimum, wrong currency) → the order is still created, checkout_url points to our page.
When the buyer opens it/pay/:id/go?provider=… checks again and redirects the buyer to the provider's own checkout.
FallbackIf the provider refuses at that moment (e.g. not available in the buyer's country), the buyer lands on our payment page with a note "PayPal is unavailable right now" and the remaining methods.

routing in the response tells you what happened: mode is direct_to_provider or payment_page; reason is null, provider_offline, provider_below_minimum, provider_currency, provider_disabled, or project_routing (picked from your project's preferred providers).

GET /v1/providers?amount=&currency= public

Methods available right now for this amount and currency, with id, name, region, status, minimum_amount and minimum_currency. The list changes — check it rather than hard-coding.

providerMethodWhere it worksMinimum*
stripeCard, Apple Pay, Google PayUS / EU2 USD
paypalPayPalUSA5 USD
revolutRevolutEU / EEA6 EUR
coinbaseCoinbase PayGlobal2 USD
cashappCash AppUSA2 USD
robinhoodRobinhoodUSA5 USD
klarnaKlarna (Pay Now)Sweden125 SEK
idealiDEALNetherlands46 EUR
banxaCard, bank transferGlobal11 USD
transakCard, bank transfer, UPIGlobal30 USD
binance, blockchaincom, topper, meld, alchemypay, sardine, particleCard via partnervaries10–30 USD

* Minimums and availability come from the payment network and change; the amount is converted to the minimum's currency before comparing. Some providers may ask the buyer for a quick one-time identity check.

Currencies & what you receive

currency is the currency the buyer pays in. Whatever it is, the money is converted by the provider and moves as USDC on Polygon.

GET /v1/currencies public

Codes accepted in currencyOwn sub-balance (custody)
USD EUR GBP CAD INR SEK✓
AUD NZD CHF NOK DKK PLN CZK HUF TRY UAH JPY SGD HKD AED MYR THB PHP IDR BRL MXN COP ZAR NGNcredited in USD

Any other code returns 400. A provider may show the buyer the price converted into its own currency (e.g. Revolut in EUR) — the buyer still pays the equivalent of your amount. Some methods accept only certain currencies; for those, /v1/providers?currency=… leaves them out.

What arrives where

ModeYou receiveExample: 100 EUR order
No account (/v1/pay)USDC on Polygon straight to payout_wallet, 95.5% of the USDC the provider delivered≈ 100 EUR in USD × 0.955 ≈ 107 USDC (at 1 EUR ≈ 1.12 USD)
Account · DirectSame — USDC on Polygon to the project's wallet; the dashboard shows the order in EUR≈ 107 USDC on-chain
Account · Stored on UTOPAYInternal balance in the order's currency (amount × 95.5%) if it has its own sub-balance; otherwise USD at the current rate95.50 EUR on the EUR balance (a 100 BRL order → ≈ 19 USD on the USD balance)

The exact USDC that arrived is in the order's value_coin after payment; it can differ slightly from the estimate because the provider sets the conversion rate. Stored balances can be exchanged between currencies and withdrawn in USDC, USDT, BTC, ETH and more — see Stored on UTOPAY.

Payment status

GET /v1/payments/:id Bearer sk

The order in the same shape. While it is not paid, we also check the live status upstream, so polling is reliable even if a callback is delayed — the balance credit and your webhook happen exactly once either way. Only the key's own project is visible (404 otherwise).

GET /v1/payments?limit=&offset=&status= Bearer sk

The project's orders, newest first. limit 1–500 (default 50), status = pending | paid | failed.

GET /api/checkout/:id/status public

No key: { "id", "status", "amount", "currency", "updated_at" }. Use it for orders created without an account. Don't treat a visit to success_url as proof of payment — check the status.

Crypto gateway

The buyer pays directly in crypto — USDT, USDC, ETH, BTC and more — to a unique address of the order. Everything that arrives is forwarded on-chain to your wallet at once. Fee 1.5%, deducted from the received amount; the blockchain fee is extra. Always direct: crypto is never kept on the UTOPAY balance.

GET /v1/crypto/coins public

Every coin/network with ticker (what you pass as coin, e.g. polygon/usdc, bep20/usdt, erc20/usdt, base/usdc, eth, btc, trc20/usdt), network, family (address type), minimum and available. EVM networks are live; networks with available: false (Tron, Bitcoin, Solana, TON…) are being connected.

POST /v1/crypto/payments Bearer sk · POST /v1/crypto/pay no account

FieldNotes
coin ✓Ticker from /v1/crypto/coins
amount + currencyPrice in fiat (any of the currencies), converted to the coin at the current rate
amount_coinOr the exact amount in the coin (instead of amount)
payout_addressYour wallet in that network. Required for /v1/crypto/pay; with a key it defaults to the project's Crypto gateway wallets (for EVM — the payout wallet). Validated per network.
email, description, success_url, cancel_urlAs in Create a payment
cURL
curl -X POST https://api.utopay.app/v1/crypto/pay \
  -H "content-type: application/json" \
  -d '{ "coin": "bep20/usdt", "amount": 100, "currency": "USD",
        "payout_address": "0xYourWallet0000000000000000000000000000000" }'
# → { "id": "ord_…", "kind": "crypto", "checkout_url": "https://api.utopay.app/pay/ord_…",
#     "crypto": { "coin": "bep20/usdt", "network": "BNB Smart Chain (BEP-20)", "symbol": "USDT",
#                 "address": "0x…", "amount_coin": 100.02, "minimum": 1.66, "detected": false, "underpaid": false } }
  • Redirect the buyer to checkout_url (amount, network, address, QR, live status) — or show crypto.address and crypto.amount_coin on your own page.
  • Statuses: pending (with crypto.detected: true once the transfer is seen, before confirmations) → paid. If less than the requested amount arrives (more than 1% short) the status is underpaid and the event is payment.underpaid — the money is already in your wallet, decide yourself.
  • Webhooks, GET /v1/payments/:id and test keys work as for card payments. Transfers below minimum or in another coin/network are not processed — the payment page warns the buyer.

Integration examples

Create the order on your server (never with a secret key in the browser), redirect the buyer to checkout_url, and mark the order paid from the webhook. Step-by-step guide: accept crypto on a website; networks and tickers for Tether: accept USDT.

Node.js — create order
const res = await fetch('https://api.utopay.app/v1/crypto/payments', {
  method: 'POST',
  headers: { authorization: `Bearer ${process.env.UTOPAY_SK}`, 'content-type': 'application/json' },
  body: JSON.stringify({ coin: 'bep20/usdt', amount: 49.9, currency: 'EUR', description: 'Order #1042',
    success_url: 'https://shop.example/thanks', cancel_url: 'https://shop.example/cart' })
});
const order = await res.json();
if (!res.ok) throw new Error(order.error);
// save order.id with your order, then redirect the buyer
reply.redirect(order.checkout_url);
Python — create order
import os, requests

r = requests.post('https://api.utopay.app/v1/crypto/payments',
    headers={'Authorization': f'Bearer {os.environ["UTOPAY_SK"]}'},
    json={'coin': 'erc20/usdt', 'amount': 120, 'currency': 'USD', 'description': 'Invoice 77'},
    timeout=20)
order = r.json()
r.raise_for_status()
print(order['id'], order['checkout_url'], order['crypto']['address'], order['crypto']['amount_coin'])
PHP — create order
$ch = curl_init('https://api.utopay.app/v1/crypto/payments');
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('UTOPAY_SK'), 'Content-Type: application/json'],
  CURLOPT_POSTFIELDS => json_encode(['coin' => 'polygon/usdt', 'amount' => 25, 'currency' => 'USD']),
]);
$order = json_decode(curl_exec($ch), true);
header('Location: ' . $order['checkout_url']);

Direct settlement

The money never rests with us: every payment is split on-chain the moment the USDC arrives.

  • Enable: dashboard → project → Funds & payouts → Direct to wallet, paste a USDC (Polygon) address. Or per request: "settlement": "direct".
  • You receive USDC on every payment; history, stats, webhooks and Telegram alerts on top. Nothing to withdraw.
  • One wallet per project — need several wallets, create several projects.

Stored on UTOPAY

The USDC lands on the UTOPAY platform wallet. On paid your share is credited to the internal balance — then you hold, exchange, swap or withdraw.

  • Enable: Funds & payouts → Stored on UTOPAY (no wallet needed), or "settlement": "custody" per request.
  • Credit: amount × 95.5% in the order's currency (USD, EUR, GBP, CAD, INR, SEK), any other currency in USD at the current rate. Credited exactly once.
  • One balance per account: all projects in this mode add up; the ledger shows the source project.
Endpoint (dashboard session)Purpose
GET /api/merchants/:id/balancesBalance per currency
GET /api/merchants/:id/balances/transactionsLedger
POST /api/merchants/:id/balances/exchange{ from_currency, to_currency, amount } — at the live rate
POST /api/merchants/:id/balances/withdraw{ currency, amount, network, to_address }
GET /api/merchants/:id/balances/withdrawalsWithdrawal requests

Withdrawal networks: USDC_POLYGON, USDT_POLYGON, USDT_TRC20, USDT_ERC20, ETH, BTC, BNB, TRX. The amount is reserved at once (pending); the UTOPAY team sends the transfer, usually within one business day (done with txid). Rejected requests are refunded. Withdrawals can't be started with an API key.

Webhooks & alerts

With an account. When an order changes state, UTOPAY POSTs it to every endpoint of the project, signed with HMAC-SHA256.

Register: dashboard → Webhooks → + Endpoint, or POST /api/merchants/:id/webhooks with { "url": "https://you.com/hooks/utopay" }. The whsec_… secret is shown in the dashboard.

EventWhen
payment.succeededFunds received (direct and stored)
payment.expiredNot paid within 24 h (sent to endpoints subscribed to payment.failed; not sent to Telegram). A late payment afterwards still sends payment.succeeded.
payment.underpaidCrypto order received less than requested (sent to endpoints subscribed to payment.succeeded)
payment.failedFailed payment (currently from test-mode simulations)
Incoming request
POST https://you.com/hooks/utopay
content-type: application/json
x-utopay-event: payment.succeeded
x-utopay-signature: t=1759660123,v1=<hex hmac-sha256>
user-agent: UTOPAY-Webhooks/1.0

{
  "event": "payment.succeeded",
  "created_at": 1759660123456,
  "data": { /* the Order object */ }
}
Node.js — verify
import crypto from 'node:crypto';
// use the RAW body, not re-serialised JSON
function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  const a = Buffer.from(v1 || ''), b = Buffer.from(expected);
  return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}
PHP — verify
// raw body + x-utopay-signature header + your whsec_… secret
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_UTOPAY_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $raw, getenv('UTOPAY_WHSEC'));
if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int)($sig['t'] ?? 0)) > 300) {
  http_response_code(400); exit;
}
$event = json_decode($raw, true);
if ($event['event'] === 'payment.succeeded') { /* mark $event['data']['id'] as paid */ }
http_response_code(200);
Python — verify
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split('=', 1) for p in header.split(','))
    t, v1 = parts.get('t', ''), parts.get('v1', '')
    expected = hmac.new(secret.encode(), f'{t}.'.encode() + raw_body, hashlib.sha256).hexdigest()
    fresh = t.isdigit() and abs(time.time() - int(t)) < 300
    return fresh and hmac.compare_digest(expected, v1)
  • Reply with any 2xx within 10 s. Retries after 30 s, 2 min, 15 min, 1 h, 6 h — 6 attempts. Deliveries are visible in the dashboard.
  • Be idempotent — de-duplicate by data.id.
  • Telegram: paid orders arrive in your chat with amount, description and id (Notifications).

Test mode

  1. Create a test secret key (utopay_sk_test_…) and call POST /v1/payments as usual — all validation (currency, provider) is the same.
  2. checkout_url points to a sandbox page (/sandbox/ord_test_…). Press Simulate payment or Simulate failure.
  3. Webhooks and Telegram fire as in production with "is_test": true. No money moves, no balance is credited.

Team, branding, routing

Team: invite by email (Team). Roles admin, developer, finance, viewer; the creator is owner.

Branding: name, logo, background and button colour in Settings — applied to the payment page, link pages and the paid screen.

Project routing: in Routing list preferred providers by priority. For API payments without provider, the first one that is online and fits the amount and currency is used (routing.reason = "project_routing"); none fits — the buyer chooses on our page. An explicit provider always wins.

Order object

Returned by create/get/list and inside webhooks.

FieldMeaning
idord_… (ord_test_… in test mode)
statuspending → paid (or failed; crypto: underpaid). Not paid within 24 h → expired: no new payment can be started, but a late payment still turns it into paid.
is_testCreated with a test key
amount, currencyWhat the buyer pays
email, descriptionAs sent
checkout_urlWhere to send the buyer (to the provider or to our page)
payment_page_urlAlways our payment page with all methods
provider, routingChosen method and how the buyer is routed — see routing
settlement, payout_walletdirect (your wallet) or custody (platform wallet)
payout_coin, payout_networkUSDC, polygon
feesSplit as fractions: merchant (your share, 0.955), affiliate, sub_affiliate, sub_affiliate2
value_coin, value_forwarded_coin, coinUSDC actually received / forwarded (after payment)
txid_in, txid_outPolygon transaction hashes
polygon_address_inOne-time receiving address of this order
expires_atWhen an unpaid order becomes expired (Unix ms)
success_url, cancel_url, created_at, updated_atAs sent / Unix ms

All public endpoints

EndpointAuthPurpose
POST /v1/pay—Payment without an account
POST /v1/paymentsskCreate a payment
GET /v1/payments/:idskOne payment (live status check)
GET /v1/paymentsskList payments
GET /v1/providers?amount=&currency=—Methods available now
GET /v1/crypto/coins—Crypto gateway coins and networks
POST /v1/crypto/payments / POST /v1/crypto/paysk / —Crypto payment with / without an account
GET /v1/currencies—Accepted currencies
GET /api/checkout/:id/status—Status of any order
GET /pay/:id, GET /pay/:id/go?provider=—Payment page / redirect to a method (for the buyer)
GET /i/:linkId—Payment link page (for the buyer)

Fees

4.5%
per payment — processing included
  • You receive 95.5% — the same with and without an account, in direct and stored modes, for any currency and method. No setup or monthly fees.
  • Large merchants can get an individual rate — contact us. The exact split of each order is in fees.

Referral program

  • Level 1: 25% of our fee from merchants you invited; level 2: 10% from merchants they invited.
  • Paid on-chain at each payment to the referral wallet set in Referrals. It comes out of our fee — referrals pay nothing extra.

Errors

Every error is { "error": "message" } with a matching HTTP status.

StatusMeaning
400Bad input: amount, unsupported currency, unknown provider, invalid payout_wallet, non-http(s) success_url, bad settlement, direct mode without a wallet, insufficient balance.
401Missing, malformed or revoked key; no dashboard session.
403pk key where sk is needed; inactive project; no access.
404Not found, or belongs to another project.
429Too many sign-in / sign-up attempts — retry in a minute.
503Payment network temporarily unavailable — retry with backoff.
500Server misconfiguration — contact support.