API reference
Last updated: 2026-10-15
How it works
- Card, Apple Pay and Google Pay. You create an order, then redirect the buyer. A licensed payment provider takes the card and buys crypto for the order; the crypto is forwarded to your wallet minus the 3% platform fee and network costs. The provider also keeps its own spread, which depends on the route the payment takes (see What you receive). The buyer sees our page first, then the provider's own pages.
- Direct crypto. You create a receiving address per order and show it to the buyer. What they send is forwarded to your wallet on the same chain, in the same coin, minus the 3% platform fee and network costs.
- No custody. Payouts go straight to the wallet you pass in each call. We never hold your funds and there is no merchant balance or withdrawal.
| Host | Used for |
|---|---|
| https://api.payster.net | Server-side API calls (create, status, coins, providers). |
| https://pay.payster.net | Buyer-facing checkout pages you redirect to. |
Quick start
- Use a self-custodial EVM wallet you control (see What you receive). Never an exchange deposit address.
- For each order, call
/v1/card/walletfrom your server with your wallet and a callback URL that contains your order reference. Storeipn_token(andcallback_secretif you have no API key) with the order. - Redirect the buyer to
https://pay.payster.net/v1/checkoutwithaddress_inandcheckout_token. - Verify the signature on your callback, check the amount, then mark the order paid.
# 1. Create a receiving address for this order (server side)
curl -G "https://api.payster.net/v1/card/wallet" \
-H "Authorization: Bearer $API_KEY" \
--data-urlencode "address=0xYOUR_EVM_PAYOUT_WALLET" \
--data-urlencode "callback=https://shop.example/paid?order=10482"
# Response
# { "address_in": "<encrypted, already URL-encoded>",
# "polygon_address_in": "0x...", "callback_url": "https://shop.example/paid?order=10482",
# "ipn_token": "...", "checkout_token": "v1....", "checkout_expires_at": 1792607263 }
# (without an API key the response also carries "callback_secret")
# 2. Redirect the customer (full redirect, never an iframe)
https://pay.payster.net/v1/checkout?address=<address_in>&checkout_token=<checkout_token>&amount=25¤cy=USD&provider=auto&email=buyer%40shop.example
# 3. Your callback receives a signed GET:
# GET https://shop.example/paid?order=10482&value_coin=...&coin=...&txid_in=...&txid_out=...
# X-Payster-Signature: t=1792000000,v1=<hex HMAC-SHA256>
const API = "https://api.payster.net";
const CHECKOUT = "https://pay.payster.net";
export async function createCheckoutUrl(order) {
// 1. one receiving address per order
const q = new URLSearchParams({
address: process.env.PAYOUT_WALLET, // your self-custodial EVM wallet
callback: `https://shop.example/paid?order=${order.id}`,
});
const res = await fetch(`${API}/v1/card/wallet?${q}`, {
headers: { Authorization: `Bearer ${process.env.PAYSTER_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
await saveOrder(order.id, { ipn_token: data.ipn_token }); // keep for status checks
// 2. send the customer here with a full redirect (address_in is already URL-encoded)
return (
`${CHECKOUT}/v1/checkout?address=${data.address_in}` +
`&checkout_token=${encodeURIComponent(data.checkout_token)}` +
`&amount=${order.total}¤cy=USD&provider=auto` +
`&email=${encodeURIComponent(order.email)}`
);
}
<?php
$api = "https://api.payster.net";
$checkout = "https://pay.payster.net";
// 1. one receiving address per order
$query = http_build_query([
"address" => getenv("PAYOUT_WALLET"), // your self-custodial EVM wallet
"callback" => "https://shop.example/paid?order=" . $order["id"],
]);
$ctx = stream_context_create(["http" => [
"header" => "Authorization: Bearer " . getenv("PAYSTER_API_KEY"),
"ignore_errors" => true,
]]);
$res = json_decode(file_get_contents("$api/v1/card/wallet?$query", false, $ctx), true);
if (empty($res["address_in"])) { throw new Exception($res["code"] ?? "wallet_not_created"); }
save_order($order["id"], ["ipn_token" => $res["ipn_token"]]);
// 2. full redirect to checkout (address_in is already URL-encoded)
header("Location: $checkout/v1/checkout?address={$res['address_in']}"
. "&checkout_token=" . urlencode($res["checkout_token"])
. "&amount={$order['total']}¤cy=USD&provider=auto"
. "&email=" . urlencode($order["email"]));
import os, requests
from urllib.parse import quote
API = "https://api.payster.net"
CHECKOUT = "https://pay.payster.net"
def create_checkout_url(order):
# 1. one receiving address per order
r = requests.get(f"{API}/v1/card/wallet", params={
"address": os.environ["PAYOUT_WALLET"], # your self-custodial EVM wallet
"callback": f"https://shop.example/paid?order={order['id']}",
}, headers={"Authorization": f"Bearer {os.environ['PAYSTER_API_KEY']}"}, timeout=20)
data = r.json()
if not r.ok:
raise RuntimeError(f"{data['code']}: {data['error']}")
save_order(order["id"], ipn_token=data["ipn_token"])
# 2. full redirect (address_in is already URL-encoded)
return (f"{CHECKOUT}/v1/checkout?address={data['address_in']}"
f"&checkout_token={quote(data['checkout_token'], safe='')}"
f"&amount={order['total']}¤cy=USD&provider=auto"
f"&email={quote(order['email'], safe='')}")
API keys and rate limits
An API key is optional. Without one, limits apply per IP address. With one, limits apply per key and your callbacks are signed with a single webhook secret instead of a secret per order. Send the key from your server only, as Authorization: Bearer psk_live_... (or X-Payster-Key). Browsers cannot send it. To get a key, ask support; you receive PAYSTER_API_KEY and PAYSTER_WEBHOOK_SECRET privately. A wrong or revoked key answers 401, it never falls back to the anonymous limits.
| Who | Order creation | Other calls |
|---|---|---|
| No key (per IP) | 10 per minute | 120 per minute |
| API key (per key) | 600 per minute | 3000 per minute |
Over the limit you get 429 with code: rate_limited and Retry-After: 60. Counters are approximate over short bursts, so back off on the first 429. Need more? Ask support.
Card, Apple Pay and Google Pay
GET https://api.payster.net/v1/card/wallet
Params: address your EVM payout wallet; callback your https URL with a query that is unique per order (URL-encode it).
Returns: address_in (encrypted, already URL-encoded), polygon_address_in (the order's on-chain receiving address), callback_url, ipn_token, checkout_token, checkout_expires_at (unix seconds, 7 days), and callback_secret when no API key is used.
Every call creates a new order. Create one per order and keep it.
GET https://pay.payster.net/v1/checkout
Params: address (address_in), checkout_token, amount, currency (USD, EUR, GBP, CAD and others per method), provider, email (the buyer's).
provider: auto picks the best card method for the buyer's device (Apple Pay on iPhone, Google Pay on Android). Or force applepay, googlepay, creditcard, or any id from /v1/providers.
Card and Apple Pay routing: for USD payments from $30 to $5,000 we send applepay and creditcard to our card partner whenever it is quoting for the buyer's country; that route pays out USDC on Polygon and asks the buyer for less before payment. Below $30, above $5,000, in any other currency, on googlepay, or if the partner does not quote, the payment uses the standard card provider. The two routes do not cost the same: see What you receive. To choose yourself, add route=standard or route=mercuryo (a pinned route skips the amount and quote checks). The X-Payster-Route response header shows which route a link took and, when it fell back, why.
For card methods, pass the buyer's email. Without it we show a short Payster page that asks for it and lets the buyer choose the method, because the card provider needs an email to start the order. The link only works with its checkout_token; links expire after 7 days.
GET https://pay.payster.net/v1/hosted
Same params without provider: a hosted page where the buyer picks any method.
GET https://api.payster.net/v1/providers
Live list of methods: id, provider_name, status, minimum_currency, minimum_amount. Read it before offering a method; the list can change without notice.
Minimums on 2026-10-15, read from /v1/providers: Apple Pay and Google Pay $10, card $6. The card partner route starts at $30 and stops at $5,000; amounts outside that band still work, they just take the standard provider. Several other methods need the buyer to log in to their own account (PayPal, Coinbase, Revolut, Binance and others) or are limited to one country. What the buyer sees after our page belongs to the provider: it says that the buyer is buying crypto, shows the provider's name, and may ask for identity documents. It is a full redirect and must stay one.
Direct crypto
GET https://api.payster.net/v1/crypto/{chain}/{coin}/wallet
Native coins have no coin segment: /v1/crypto/btc/wallet, /v1/crypto/eth/wallet, /v1/crypto/trx/wallet.
Params: address your wallet on that chain (0x for EVM chains, T... for TRON, a Solana or Bitcoin address); callback; optional pending=1 (also call when the payment is first seen) and confirmations (1 to 99).
Returns: address_in (show this to the buyer), callback_url, ipn_token, and callback_secret when no API key is used.
GET https://api.payster.net/v1/crypto/{chain}/{coin}/info | /fees | /convert?from=usd&value=25
info: coin name, minimum (what the buyer must send at least) and prices. fees: estimated_cost per payment in the coin, and estimated_cost_currency in fiat. convert: value_coin and exchange_rate. https://api.payster.net/v1/coins lists every enabled coin with minimum_transaction_coin and logo.
Enabled stablecoins and native coins, with live values from 2026-10-14. The minimum is per payment; a smaller payment cannot be forwarded, so never let a buyer send less. Network costs are deducted from what you receive.
| Chain | Path | Stablecoins | Minimum | Network cost per payment |
|---|---|---|---|---|
| TRON | trc20/usdt | USDT | 16.70 | about $9.00 |
| Ethereum | erc20/usdt, erc20/usdc | USDT, USDC | 3.34 | about $1.65 |
| BNB Smart Chain | bep20/usdt, bep20/usdc | USDT, USDC | 1.67 | about $0.03 |
| Polygon | polygon/usdt, polygon/usdc | USDT, USDC | 0.835 | about $0.02 |
| Solana | sol/usdt, sol/usdc | USDT, USDC | 1.67 | under $0.01 |
| Arbitrum | arbitrum/usdt0, arbitrum/usdc | USDT0, USDC | 1.67 | about $0.02 |
| Optimism | optimism/usdt, optimism/usdc | USDT, USDC | 1.67 | about $0.01 |
| Avalanche C-Chain | avax-c/usdt, avax-c/usdc | USDT, USDC | 1.67 | about $0.03 |
| Linea | linea/usdt, linea/usdc | USDT, USDC | 1.67 | about $0.80 |
| Base | base/usdt, base/usdc | USDT, USDC | 5.01 | about $1.39 |
| Monad | monad/usdt0, monad/usdc | USDT0, USDC | 1.67 | under $0.01 |
| Bitcoin | btc | BTC | 0.0001336 BTC | about $0.23 |
| Ethereum (native) | eth | ETH | 0.002505 ETH | about $0.48 |
| TRON (native) | trx | TRX | 16.7 TRX | about $2.00 |
To show a QR code, render the address_in yourself (any QR library). For EVM tokens, show the address and tell the buyer which chain and coin to send: a wrong chain or coin is not recoverable.
Callbacks and signatures
When a payment completes we call your callback URL with a GET request: all your own query parameters, plus the payment fields below, signed with HMAC-SHA256.
| Field | Meaning |
|---|---|
| value_coin | Amount that reached the order's receiving address, in coin. |
| coin | What arrived. Card payments: usually polygon_usdc or polygon_usdt. Direct crypto: the coin that was paid. |
| txid_in | Transaction that paid the order. Use it as your idempotency key. |
| txid_out | Transaction that forwarded the payout to your wallet. |
| address_in | The order's receiving address on chain (card: the polygon_address_in from the wallet call). |
| value_forwarded_coin | Total forwarded from the receiving address to all payout wallets. |
| address_out | URL-encoded JSON {wallet: share}. Your amount = value_forwarded_coin x your share. |
| pending | Crypto only. 1 = seen but not yet confirmed (only if you set pending=1). 0 = confirmed. |
| Header | Value |
|---|---|
| X-Payster-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of t + "." + the exact URL we called> |
| X-Payster-Idempotency-Key | txid_in for a confirmed payment, txid_in:pending for a pending notice. |
| X-Payster-Event | payment.paid or payment.pending |
| X-Payster-Delivery-Attempt | 1, 2 or 3 |
| User-Agent | Payster-Callback/1.0 |
Secret. With an API key: your webhook secret. Without one: the callback_secret returned when that order was created. Signed string: t, a dot, then the exact URL we requested: the origin and path of your callback_url plus the raw query string as it arrived. Do not rebuild the query from parsed parameters; one changed byte breaks the signature. Reject anything that does not verify, and anything with t older than 5 minutes.
import crypto from "node:crypto";
import express from "express";
// Your callback URL without its query: origin + path of callback_url.
const CALLBACK_BASE = "https://shop.example/paid";
const app = express();
app.get("/paid", async (req, res) => {
// API-key merchants: one webhook secret. Without a key: the order's callback_secret.
const secret = process.env.PAYSTER_WEBHOOK_SECRET ?? (await getOrder(req.query.order)).callback_secret;
// Sign input = timestamp + "." + the exact URL we called. Use the RAW query string.
const i = req.originalUrl.indexOf("?");
const url = CALLBACK_BASE + (i === -1 ? "" : req.originalUrl.slice(i));
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(req.get("X-Payster-Signature") || "");
if (!m) return res.sendStatus(403);
const expected = crypto.createHmac("sha256", secret).update(`${m[1]}.${url}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(m[1])) < 300;
if (!fresh || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]))) return res.sendStatus(403);
// Idempotent: the same payment can arrive more than once.
const key = req.get("X-Payster-Idempotency-Key"); // txid_in, or txid_in:pending
if (req.query.pending !== "1" && !(await alreadyProcessed(key))) {
await markPaid(req.query.order, req.query.value_coin, req.query.coin, req.query.txid_out, key);
}
res.sendStatus(200); // any 2xx ends delivery
});
<?php
// Your callback URL without its query: origin + path of callback_url.
$base = "https://shop.example/paid.php";
// API-key merchants: one webhook secret. Without a key: the order's callback_secret.
$secret = getenv("PAYSTER_WEBHOOK_SECRET") ?: get_order($_GET["order"])["callback_secret"];
$qs = $_SERVER["QUERY_STRING"] ?? ""; // raw, exactly as received
$url = $qs === "" ? $base : "$base?$qs";
if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $_SERVER["HTTP_X_PAYSTER_SIGNATURE"] ?? "", $m)) {
http_response_code(403); exit;
}
$expected = hash_hmac("sha256", $m[1] . "." . $url, $secret);
if (!hash_equals($expected, $m[2]) || abs(time() - (int)$m[1]) > 300) {
http_response_code(403); exit;
}
$key = $_SERVER["HTTP_X_PAYSTER_IDEMPOTENCY_KEY"] ?? ""; // txid_in, or txid_in:pending
if (($_GET["pending"] ?? "0") !== "1" && !already_processed($key)) {
mark_paid($_GET["order"], $_GET["value_coin"], $_GET["coin"], $_GET["txid_out"], $key);
}
http_response_code(200);
import hashlib, hmac, os, re, time
from flask import Flask, abort, request
# Your callback URL without its query: origin + path of callback_url.
CALLBACK_BASE = "https://shop.example/paid"
app = Flask(__name__)
@app.get("/paid")
def paid():
# API-key merchants: one webhook secret. Without a key: the order's callback_secret.
secret = os.environ.get("PAYSTER_WEBHOOK_SECRET") or get_order(request.args["order"])["callback_secret"]
qs = request.query_string.decode() # raw bytes as received; request.args is NOT raw
url = f"{CALLBACK_BASE}?{qs}" if qs else CALLBACK_BASE
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", request.headers.get("X-Payster-Signature", ""))
if not m:
abort(403)
expected = hmac.new(secret.encode(), f"{m[1]}.{url}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, m[2]) or abs(time.time() - int(m[1])) > 300:
abort(403)
key = request.headers.get("X-Payster-Idempotency-Key", "") # txid_in, or txid_in:pending
if request.args.get("pending") != "1" and not already_processed(key):
mark_paid(request.args["order"], request.args["value_coin"], request.args["coin"],
request.args["txid_out"], key)
return "", 200
Delivery and retries. Answer with any 2xx within 8 seconds. Otherwise we retry twice (after 1 and 3 seconds). A callback can still arrive more than once, so process each X-Payster-Idempotency-Key once. If all attempts fail, the payout is not affected: it already went to your wallet on chain. Use the status endpoint to reconcile.
Before you credit: check that the order exists and is unpaid, compare value_coin (and coin) with what you expected, and for crypto with pending=1, wait for the pending=0 call.
Payment status
GET https://api.payster.net/v1/status?ipn_token=...&ticker={chain}/{coin}
ticker only for direct crypto (for example polygon/usdc or btc); leave it out for card orders.
Returns: status (paid or unpaid), and once paid value_coin, coin, txid_out (crypto also txid_in).
Use it to reconcile or answer support questions, not as a polling loop: callbacks are the trigger to mark an order paid.
Errors
Every error is JSON { "error": "human text", "code": "stable_code" }. Branch on code, show error. Checkout pages answer with an HTML page instead.
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_address | The payout wallet is not valid for that chain. |
| 400 | invalid_callback | callback is not a public https URL with a query, or is longer than 1024 characters. |
| 400 | merchant_wallet_not_allowed | This wallet cannot receive payouts. |
| 400 | invalid_ipn_token | Status call without a valid ipn_token. |
| 400 | request_rejected | The payment network refused the parameters (amount, currency or method). |
| 401 | invalid_api_key | The key is malformed, unknown or revoked. Calls without a key are not affected. |
| 403 | endpoint_not_available | This operation is not offered. |
| 404 | not_found / coin_not_available | Unknown path, or a chain or coin that is not enabled. |
| 404 (HTML) | Checkout link missing, altered or expired. Create a new order. | |
| 405 | method_not_allowed | Use GET. |
| 429 | rate_limited | Too many calls. Wait for Retry-After (60 seconds). |
| 502 | upstream_unavailable / wallet_not_created | The payment network did not answer properly. Retry with backoff. |
| 503 | links_not_live / fee_config_invalid / checkout_signing_not_configured / callback_relay_not_configured / rate_limit_unavailable | Temporarily unavailable on our side. Retry later and tell support if it lasts. |
What you receive
- Card, Apple Pay and Google Pay: the provider decides which crypto it buys. Payouts may arrive as USDC or USDT (Polygon or BNB Chain), ETH, or POL; card and Apple Pay on the partner route always pay USDC on Polygon. Use one self-custodial EVM wallet that you control on Polygon, Ethereum and BNB Chain; the same 0x address receives all of them. Read
coinin the callback to see what arrived. - Direct crypto: the same coin on the same chain that the buyer sent.
- Fees: 3% platform fee per payment, plus network costs. On card methods the provider also charges its own fee or spread, shown to the buyer at checkout before they pay. That spread is the provider's, not ours, and it moves with method, country and amount, so treat the figures below as measured examples rather than a quote.
| Card payment | Provider spread we measured | You receive, after the platform fee |
|---|---|---|
| Card or Apple Pay, USD $30 to $5,000 (partner route) | about 5.6% to 5.9% | about 91% |
| Google Pay, any amount | about 3.9% | about 93% |
| Card or Apple Pay under $30, over $5,000, or not in USD | about 3.9% | about 93% |
| Direct crypto | none | 97%, less the network cost |
Measured on 2026-10-15 for the partner route ($30 to $5,000, five countries) and on 2026-10-14 for the standard provider. The partner route costs the merchant about two points more; it is the default on that band because it asks the buyer for far less before payment, which is what makes small payments complete. Pin route=standard on a link if you would rather keep the cheaper spread.
Identity checks and limits
Card methods are run by licensed providers. They decide, per country, amount, method and buyer history, whether to ask the buyer for identity documents, and their limits can be per payment or cumulative. We do not run identity checks, cannot switch them off, and cannot promise any amount without them: in our own tests a $10 payment from some countries already required an ID document and a selfie. Large single payments may be refused by the provider. Direct crypto payments involve no provider checkout.
What we measured on 2026-10-15, passively, from the buyer's side, without paying:
| Route | What the buyer was asked for before paying |
|---|---|
| Card partner, Apple Pay, $30 to about $700 (tested in Turkey, Germany, the UK) | Nothing: the partner went straight to the order summary and the Apple Pay sheet. No email, no name, no document. |
| Card partner, Apple Pay, about $850 and above | An email address first, then the order summary. |
| Standard provider, any amount from $10 to $15,000 | Name, phone, date of birth and address, on an identical form at every amount. In Turkey and the UAE it then asked for an ID document and a selfie even at $10; the provider's published rules put the UK and Australia in the same group. In the EU and the US a first-time buyer can pass without a document, but only up to about $760 and only once. |
Read these limits carefully. They are a snapshot of what the provider asked our test buyers for, in those countries, on that day. They are not a promise, and they say nothing about what happens after the Apple Pay sheet or after the email: we have not put a real payment through, so a later identity step on the partner route is possible. The provider can add checks at any time, per country, per amount and per buyer. This is a lighter check, not "no KYC". Per-payment ceilings on 2026-10-15: $5,000 on the partner route (above that the payment takes the standard provider, which we have seen accept up to $15,000).
Testing
There is no sandbox: every order is real. Test with small real payments to your own wallet.
- Direct crypto: create
/v1/crypto/polygon/usdc/walletwith your wallet, send 1 USDC on Polygon toaddress_in, and check that your callback verifies,/v1/statussayspaid, and the payout reached your wallet. - Card: create a $30 order and pay it yourself with Apple Pay, which exercises the partner route, then check the same three things and that
coincame back as USDC on Polygon. A $10 order instead exercises the standard provider, where you should expect the identity step. - Signature: replay a captured callback with one changed parameter and confirm your endpoint answers 403.
Checkout rules
- Never embed checkout in an iframe. Always redirect the buyer with a full page load.
- Describe what the buyer is paying for honestly; never disguise the nature of a transaction.
- Respect each method's minimum amount.
- Your business must not appear on the prohibited activities list. See also pricing.
- Plugins (WooCommerce, WHMCS, PrestaShop, OpenCart): ask support for a build. Sellers without a website can use payment links.
Changelog
Paths without /v1 from earlier versions keep working, but new integrations should use /v1.
| Date | Change |
|---|---|
| 2026-10-15 | Card and Apple Pay from $30 to $5,000 (USD) now go to our card partner, which pays out USDC on Polygon and asks the buyer for less up front. Google Pay, other amounts and other currencies are unchanged. The partner's fee is higher than the standard provider's, so what you receive differs by amount band: see What you receive. Pin a route with route=standard or route=mercuryo; the X-Payster-Route header shows which route a link took. |
| 2026-10-14 | Apple Pay from $30 to $5,000 (USD) tried a light identity-check route before the standard card provider. Superseded on 2026-10-15: that route's provider stopped accepting our links, so it is no longer offered. |
| 2026-10-14 | /v1 paths for every call (older paths keep working). Signed callbacks with an idempotency key and retries. API keys with higher limits. One status endpoint for card and crypto. A stable code on every error. Coin logos served from our own host. The qrcode endpoint was removed: render QR codes yourself. |