# TrezorPay > Accept BTC, USDT, USDC, ETH, SOL, LTC, DOGE, BNB and TRX on thirteen networks. TrezorPay forwards every payment straight to your own wallet, minus 1.25%. No account, no API key. Base URL: https://api.trezorpay.com. Every endpoint is a GET with query parameters and a path ending in "/". No API key: the callback URL identifies a payment address. # Build with AI TrezorPay is small enough to integrate in one sitting, and an AI coding agent can do most of it for you. Give it the prompt below together with a description of your stack (framework, database, how you deploy), and it builds the client, the callback endpoint, the migrations and the tests. ## What the prompt covers - Creating payment addresses, QR codes and fiat conversions, with configuration instead of hardcoded wallets. - Receiving pending and confirmed callbacks in GET, form or JSON form. - Verifying the RSA-SHA256 `x-ca-signature`, including behind a reverse proxy. - Idempotency on `uuid` with a database unique constraint, partial payments and overpayments. - Every error message of the API and which ones to retry. - A 21-case test suite your agent writes alongside the code: signatures, flows, idempotency, money and client behaviour. ## Tips - Append your stack to the prompt, for example: _"Stack: Laravel 11, MySQL, deployed on Forge. Accept USDT on Tron and USDC on Base."_ - Ask the agent to run the test suite before it reports back, and to expose the callback URL with an HTTPS tunnel while developing: callbacks to `localhost` are refused. - The prompt is also served raw at [`/prompt.md`](/prompt.md), and the full documentation as text at [`/llms-full.txt`](/llms-full.txt), for agents that browse. # Callbacks ## When callbacks are sent | Moment | `pending` | Sent when | | ----------------- | --------- | ---------------------------------------------------- | | Payment seen | `1` | Only if you created the address with `pending=1` | | Payment forwarded | `0` | Always, once the amount has been sent to your wallet | Each payment to an address gets its own pair of callbacks. Addresses never expire. ## Transport - **GET** (default): every field is added to the query string of your callback URL. - **POST form** (`post=1`): fields in an `application/x-www-form-urlencoded` body. - **POST JSON** (`json=1`): fields in a JSON body. Your own query parameters always stay on the URL. ## Fields | Field | Description | | ---------------------- | ----------------------------------------------------------- | | `uuid` | Payment id, identical on its pending and confirmed callback | | `address_in` | The address that was paid | | `address_out` | Your wallet, or the split | | `txid_in` | Customer's transaction | | `txid_out` | Our forwarding transaction (confirmed only) | | `confirmations` | Confirmations at the time of the callback | | `value_coin` | Amount paid | | `value_forwarded_coin` | Amount sent to your wallet (confirmed only) | | `fee_coin` | Our fee (confirmed only) | | `coin` | Ticker in underscore form, such as `trc20_usdt` | | `price` | Coin price in USD | | `pending` | `1` or `0` | | `result` | `pending` or `sent` | | `value_coin_convert` | Fiat values, with `convert=1` | `value`, `value_forwarded` and `fee` are legacy integer fields: read the `_coin` strings instead. ## Retries Answer **HTTP 200** with the body **`*ok*`** within 30 seconds. Anything else is retried after 6, 12, 24 minutes and so on, doubling, until the payment is 3 days old. Every attempt is visible in [`/{ticker}/logs/`](/docs/api/payments/get-logs). ## Verifying the signature Every callback carries `x-ca-signature`: a base64 RSA-SHA256 (PKCS#1 v1.5) signature of the **full URL** for GET callbacks, or of the **raw body** for POST callbacks. Verify it with the key from [`/pubkey/`](/docs/api/utilities/get-public-key). ```js title="Node.js" import { createVerify } from 'node:crypto' export function verify(signedData, signature, pubkey) { return createVerify('sha256').update(signedData).verify(pubkey, signature, 'base64') } ``` ```python title="Python" import base64 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding def verify(signed_data: bytes, signature: str, pubkey_pem: str) -> bool: key = serialization.load_pem_public_key(pubkey_pem.encode()) try: key.verify(base64.b64decode(signature), signed_data, padding.PKCS1v15(), hashes.SHA256()) return True except Exception: return False ``` ```php title="PHP" r.json()) if (body.status !== 'success') throw new Error(body.error) return body // { address_in, minimum_transaction_coin, ... } } // const { address_in } = await createAddress({ ticker: 'trc20/usdt', orderId: 42, wallet: 'T…' }) ``` ## Receive and verify the callback ```js title="server.js" import { createVerify } from 'node:crypto' import express from 'express' const { pubkey } = await fetch('https://api.trezorpay.com/pubkey/').then((r) => r.json()) const seen = new Set() // use your database in production const app = express() app.get('/paid', (req, res) => { const signed = `https://${req.headers.host}${req.originalUrl}` const signature = req.headers['x-ca-signature'] const valid = createVerify('sha256').update(signed).verify(pubkey, signature, 'base64') if (!valid) return res.status(401).end() const { uuid, pending, order, value_forwarded_coin, coin } = req.query if (pending === '0' && !seen.has(uuid)) { seen.add(uuid) console.log(`order ${order} paid: ${value_forwarded_coin} ${coin} sent to your wallet`) } res.send('*ok*') }) app.listen(3000) ``` Behind a proxy, rebuild the URL exactly as we called it (scheme, host and query string), or the signature will not match. For `post=1` or `json=1` callbacks, verify the raw request body instead of the URL. Next: [callback fields](/docs/callbacks) and the [API reference](/docs/api/payments/create-address). # Accept crypto payments with PHP Plain PHP 8 with the `openssl` and `curl` extensions; no framework or SDK needed. ## Create a payment address ```php title="create_address.php" "https://shop.example/paid?order={$orderId}", 'address' => $wallet, 'pending' => 1, ]); $body = json_decode(file_get_contents("https://api.trezorpay.com/{$ticker}/create/?{$query}"), true); if (($body['status'] ?? '') !== 'success') { throw new RuntimeException($body['error'] ?? 'Unknown error'); } return $body; // address_in, minimum_transaction_coin, ... } ``` ## Receive and verify the callback ```php title="paid.php" dict: response = requests.get( f"{API}/{ticker}/create/", params={ "callback": f"https://shop.example/paid?order={order_id}", "address": wallet, "pending": 1, }, timeout=15, ) body = response.json() if body.get("status") != "success": raise RuntimeError(body.get("error")) return body # address_in, minimum_transaction_coin, ... ``` ## Receive and verify the callback ```python title="app.py" import base64 import requests from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding from flask import Flask, request PUBKEY = serialization.load_pem_public_key( requests.get("https://api.trezorpay.com/pubkey/", timeout=15).json()["pubkey"].encode() ) app = Flask(__name__) def is_signed_by_trezorpay(data: bytes, signature: str) -> bool: try: PUBKEY.verify(base64.b64decode(signature), data, padding.PKCS1v15(), hashes.SHA256()) return True except Exception: return False @app.get("/paid") def paid(): if not is_signed_by_trezorpay(request.url.encode(), request.headers.get("x-ca-signature", "")): return "", 401 if request.args.get("pending") == "0": order = request.args["order"] amount = request.args["value_forwarded_coin"] # mark the order paid, once per uuid return "*ok*" ``` `request.url` must be the URL exactly as we called it; behind a proxy, configure Flask's `ProxyFix`. For `post=1` or `json=1` callbacks, verify `request.get_data()` instead. # Introduction TrezorPay accepts crypto payments on Bitcoin, Litecoin, Bitcoin Cash, Dogecoin, Solana, Ethereum, BNB Smart Chain, Polygon, Base, Arbitrum, Optimism, Avalanche and Tron, and forwards each one on-chain to the wallet you choose as soon as it confirms. There is no account to open and no API key: one request gives you a deposit address, and your server is told about every payment through a signed callback. ## How it works 1. **Your server creates an address** with [`/{ticker}/create/`](/docs/api/payments/create-address), passing your callback URL and your wallet. 2. **Your customer pays it.** If you asked for it, we call your URL with `pending=1` as soon as the payment is seen. 3. **We forward it.** Once the payment has the confirmations its network needs, the amount minus our fee and the network cost is sent to your wallet. 4. **You get a signed callback** with `pending=0`, the transaction ids and the amounts. Answer `*ok*`. ## Conventions - Base URL: `https://api.trezorpay.com` - Every endpoint is a `GET` with query parameters, and every path ends with `/`. - No API key. The callback URL is the identity of a payment address: the same callback always returns the same address. - Success answers `200` with `"status": "success"`. Errors answer `400` (or `404`) with `{"status":"error","error":"…"}`. - Amounts are strings, so no precision is lost. # Quickstart ## 1. Create an address Call `/{ticker}/create/` with the URL that should be notified and the wallet that should receive the money. Put your own order id in the callback URL: it comes back on every callback. ```bash title="Terminal" curl "https://api.trezorpay.com/trc20/usdt/create/?callback=https%3A%2F%2Fshop.example%2Fpaid%3Forder%3D1042&address=TJzXZQhbhJB3dA2TmxmvyPxk2nKsmZ6yWf&pending=1" ``` ```json title="Response" { "status": "success", "address_in": "TN7Zbu4UwtKneYDJyDKXkSorVu95hsE3q4", "address_out": "TJzXZQhbhJB3dA2TmxmvyPxk2nKsmZ6yWf", "callback_url": "https://shop.example/paid?order=1042", "minimum_transaction_coin": "10.00000000", "priority": "default" } ``` Show `address_in` to your customer, with a QR code from [`/{ticker}/qrcode/`](/docs/api/utilities/get-qr-code) if you like. Payments below `minimum_transaction_coin` are recorded but never forwarded. ## 2. Receive the callback When the payment arrives (with `pending=1`) and again once it has been forwarded, we call your URL: ```text GET https://shop.example/paid?order=1042&uuid=…&address_in=TN7Z…&address_out=TJzX… &txid_in=…&txid_out=…&value_coin=250&value_forwarded_coin=244.39&fee_coin=2.5 &coin=trc20_usdt&price=1&pending=0&result=sent ``` ## 3. Verify and acknowledge Check the `x-ca-signature` header with our public key, mark the order paid when `pending=0`, and answer `*ok*`. ```js title="server.js (Express)" import { createVerify } from 'node:crypto' import express from 'express' const { pubkey } = await fetch('https://api.trezorpay.com/pubkey/').then((r) => r.json()) const app = express() app.get('/paid', (req, res) => { const url = `https://${req.headers.host}${req.originalUrl}` const valid = createVerify('sha256') .update(url) .verify(pubkey, req.headers['x-ca-signature'], 'base64') if (!valid) return res.status(401).end() if (req.query.pending === '0') { // value_forwarded_coin has reached your wallet: fulfil order req.query.order } res.send('*ok*') }) ``` A payment can be delivered more than once (retries). `uuid` is identical on the pending and the confirmed callback of one payment: store it and ignore repeats.