# NoncePay > Accept BTC, USDT, USDC, ETH, SOL, LTC, DOGE, BNB and TRX on thirteen networks. NoncePay forwards every payment straight to your own wallet, minus 1.25%. No account, no API key. Base URL: https://api.noncepay.app. 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 NoncePay 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. - Payment links: when to use the hosted page instead of your own checkout, creating a link, its callbacks, the `success_token` on the redirect, and fulfilling only on a signed callback or a paid status. - 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, plus 3 cases for payment links. ## 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" ` | | `Link expired` | `/link/{link_id}/pay/` on an expired link | | `Link unavailable` | `/link/{link_id}/pay/` on a paid or disabled link | | `Value below the coin minimum` | The link's amount is below this coin's minimum | | `Too many requests` (429) | Rate limit reached; retry after `Retry-After` seconds | ## Rate limits Limits apply per client IP: `/create/` allows 120 requests a minute and 20,000 a day, every other route 300 a minute. For [payment links](/docs/guides/payment-links), `/link/create/` allows 30 a minute and 1,000 a day, `/link/{link_id}/pay/` counts against the `/create/` limits, and `/link/{link_id}/report/` allows 5 an hour. One address per checkout stays far below them. Over a limit the API answers `429` with a `Retry-After` header giving the seconds to wait. # Fees ## Service fee | Volume forwarded to the wallet in 30 days | Fee | | ----------------------------------------- | ----- | | Under $250k | 1.25% | | $250k – $1M | 1.10% | | $1M – $2.5M | 1.00% | | $2.5M – $5M | 0.85% | | $5M – $10M | 0.70% | | $10M – $20M | 0.50% | | $20M and above | 0.25% | Volume is counted per destination wallet over the 30 days before midnight UTC, so a tier changes once a day. A split payment uses the highest fee among its destinations. There is no sign-up, monthly, minimum or withdrawal fee. ## Network cost `value_forwarded_coin = value_coin − fee_coin − network cost` - **Native coins** (ETH, BNB, POL, TRX): the gas of each forwarding transaction comes out of the payment. - **Tokens** (USDT, USDC): we send the gas ourselves and recover its cost in the token at the market price, together with the fee. [`/{ticker}/estimate/`](/docs/api/coins/estimate-cost) returns the current network cost of a forward. # Accept crypto payments with Magento The NoncePay module for Magento 2 and Adobe Commerce adds a **Pay with crypto** payment method to the checkout. The customer pays on the NoncePay hosted payment page and the order is invoiced once the payment is confirmed. Every payment is forwarded to your own wallet, minus our [fee](/docs/fees). There is no account, no API key and no withdrawal step. ## Install With Composer, from the Magento root: ```bash composer config repositories.noncepay vcs https://github.com/Nonce-pay/noncepay-magento composer require noncepay/module-payment bin/magento module:enable NoncePay_Payment bin/magento setup:upgrade bin/magento cache:flush ``` Or download `noncepay-magento.zip` from the [latest release](https://github.com/Nonce-pay/noncepay-magento/releases/latest), unzip it into the Magento root (it adds `app/code/NoncePay/Payment/`) and run the same `bin/magento` commands. In production mode, also run `setup:di:compile` and `setup:static-content:deploy`. Then go to **Stores > Configuration > Sales > Payment Methods > NoncePay (crypto)**, enter a receiving wallet for each family of coins you accept, set **Enabled** to Yes and save. Requires Magento Open Source or Adobe Commerce 2.4.6 or later and PHP 8.1 to 8.3. Your store must be reachable from the internet so NoncePay can call its callback. The source is on [GitHub](https://github.com/Nonce-pay/noncepay-magento). ## How it works 1. The customer picks **Pay with crypto** and places the order, which is created in **Pending Payment**. 2. The module creates a [payment link](/docs/guides/payment-links) for the order grand total in the order currency and redirects the customer to its page. A reload reuses the same link. If the link cannot be created, the order is cancelled and the cart restored. 3. The customer picks a coin and pays. Each payment is forwarded to your wallet as soon as it confirms. 4. Once the payments cover the order, NoncePay sends the link-paid [callback](/docs/callbacks). The module verifies its `x-ca-signature` against the key from [`/pubkey/`](/docs/api/utilities/get-public-key), checks the link and the amount, then invoices the order with the link id as transaction id and moves it to **Processing**, so a repeated callback is captured once. 5. The customer is sent back to the store, which shows the order confirmation once the order is invoiced and a "being confirmed" page until then. The order is only marked paid by the signed callback, never by this redirect. Captures, over and underpayments are written to the order history. Problems are logged to `var/log/noncepay.log`. ## Settings | Setting | Default | What it does | | ---------------------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | Wallets | empty | One per family: EVM, Tron, Bitcoin, Litecoin, Bitcoin Cash, Dogecoin, Solana; split syntax accepted | | Coins offered | every coin of your wallets | Optional comma-separated tickers such as `trc20/usdt,bep20/usdt,btc` | | Merchant name, logo | store name, none | Shown on the payment page; the logo must be an `https` URL | | Underpayment tolerance | 0% | 0 to 1%: how much less than the order total still invoices it | | Debug log | off | Logs API calls and every callback to `var/log/noncepay.log` | ## Limits - The order currency must be one NoncePay prices (USD, EUR, GBP and about thirty others). For another currency the method is hidden at checkout. - Magento cancels orders left in Pending Payment after the **Pending Payment Order Lifetime** (480 minutes by default). The payment link expires an hour before that; keep the lifetime at two hours or more. - An overpayment invoices the order and is noted in its history. An underpayment beyond the tolerance is asked for again on the payment page. - A payment that completes after the order was cancelled is still forwarded to your wallet and noted on the order, but it does not invoice it. - Refunds are sent from your wallet: NoncePay holds nothing to refund from. # Accept crypto payments with Node.js This guide uses Node.js 18+ (built-in `fetch`) and Express. No SDK and no API key are needed. ## Create a payment address ```js title="create-address.js" const API = 'https://api.noncepay.app' export async function createAddress({ ticker, orderId, wallet }) { const callback = `https://shop.example/paid?order=${orderId}` const url = new URL(`${API}/${ticker}/create/`) url.searchParams.set('callback', callback) url.searchParams.set('address', wallet) url.searchParams.set('pending', '1') const body = await fetch(url).then((r) => 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.noncepay.app/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). Runnable shops with this flow: [Express](https://github.com/Nonce-pay/noncepay-examples/tree/main/node/express) and [Next.js](https://github.com/Nonce-pay/noncepay-examples/tree/main/node/nextjs). # Payment links A payment link is a page on NoncePay that you can send by email, chat or invoice. Your customer opens it, picks a coin, pays the address shown, and watches the payment confirm. Every payment is forwarded to your own wallet as soon as it confirms, minus our fee and the network cost, exactly like an address from [`/{ticker}/create/`](/docs/api/payments/create-address): NoncePay never holds the funds. Payment links are a NoncePay extension: CryptAPI has no equivalent route. ## Create a link One request, no account and no API key. Give a wallet per chain family you accept; the page offers every coin of those families, or only the ones you list in `coins`. ```bash title="Terminal" curl "https://api.noncepay.app/link/create/?value=49.90¤cy=usd&address_tron=TJzXZQhbhJB3dA2TmxmvyPxk2nKsmZ6yWf&address_evm=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266&coins=trc20/usdt,bep20/usdt&item_description=Order%201042&callback=https%3A%2F%2Fshop.example%2Fpaid%3Forder%3D1042&redirect_url=https%3A%2F%2Fshop.example%2Fthanks" ``` ```json title="Response" { "status": "success", "link_id": "3f6c2a1e-8b4d-4c7a-9e21-5d0f7b9a6c13", "payment_url": "https://checkout.noncepay.app/?id=3f6c2a1e-8b4d-4c7a-9e21-5d0f7b9a6c13", "success_token": "q3J8vT0mZk6xR1yH5wN2bC9dL4sF7gA0eU8iO3pK6jM" } ``` Send `payment_url` to your customer, and store `link_id` and `success_token` with your order. With the [Node.js SDK](https://www.npmjs.com/package/@nonceapp/api): ```ts title="create-link.ts" import { NoncePay } from '@nonceapp/api' const noncepay = new NoncePay() const { link_id, payment_url, success_token } = await noncepay.createLink({ value: '49.90', currency: 'usd', addresses: { tron: 'TJzXZQhbhJB3dA2TmxmvyPxk2nKsmZ6yWf', evm: '0xf39F…2266' }, coins: ['trc20_usdt', 'bep20_usdt'], itemDescription: 'Order 1042', callback: 'https://shop.example/paid?order=1042', redirectUrl: 'https://shop.example/thanks', }) ``` | Parameter | Notes | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `kind` | `payment` (default) or `deposit` | | `value`, `currency` | `payment`: the amount to collect, in a fiat currency (`usd`, `eur`, …) | | `suggested_value`, `currency` | `deposit`: an amount shown on the page, never enforced | | `address_` | At least one of `address_evm`, `address_tron`, `address_btc`, `address_ltc`, `address_bch`, `address_doge`, `address_sol` | | `coins` | Comma-separated tickers to offer, such as `trc20/usdt,bep20/usdt` | | `callback` | Your URL for [callbacks](#callbacks) | | `pending`, `post`, `json` | As on `/create/` | | `redirect_url` | `payment`: where the customer is sent once paid | | `item_description` | Plain text, up to 128 characters | | `merchant_name` | Plain text, up to 64 characters | | `logo_url` | An `https` image URL, up to 2048 characters, shown beside `merchant_name`; served by you, never hosted by NoncePay | | `expire_at` | `payment`: epoch seconds, at least one hour ahead; 7 days by default | | `tolerance` | `payment`: percent, `0` to `1`, the paid total may fall short and still count as paid; `0` by default | `address_evm` covers Ethereum, BNB Smart Chain, Polygon, Base, Arbitrum, Optimism and Avalanche. Every wallet accepts the split syntax `0.7@addrA|0.3@addrB`, and is checked when the link is created, so a mistyped wallet fails before anyone pays. Every parameter is in the [API reference](/docs/api/payment-links/create-link). ## Payment or deposit - A **`payment`** link asks for a fixed fiat amount and is paid once. It ends `paid`, `expired` or `disabled`. - A **`deposit`** link has no amount, or a suggested one, and accepts any number of payments of any amount from the coin's minimum: donations, top-ups. It does not expire and has no paid state; each payment is reported on its own. ## What your customer sees 1. The page shows your logo and name, your description, the amount and the coins you offer. The logo is loaded from your URL without a referrer and shown at most 40 pixels tall on a dark background, so use a version that reads on dark. 2. The customer picks a coin. The page gets an address for that coin and, for a `payment` link, the amount in the coin at the current rate, **locked for 15 minutes**. Once the quote expires the page quotes again at the current rate. The same link and coin always give the same address. 3. The page follows the payment from seen to confirmed. When the link is paid and has a `redirect_url`, the customer is sent there with `link_id` and `success_token` added to the query string. On your `redirect_url`, compare the `success_token` in the query string with the one you stored for that `link_id`. The redirect comes from the customer's browser: do not ship an order on it. Use the signed [callback](#callbacks), or check that [`/link/{link_id}/`](/docs/api/payment-links/get-link) answers `"link_status": "paid"`. ## Callbacks With a `callback`, you receive two kinds of notification, both signed with `x-ca-signature` and retried until you answer `*ok*`. Verify them as described in [Callbacks](/docs/callbacks). - **Each deposit**: the usual [payment callback](/docs/callbacks), with `noncepay_link=` and `noncepay_ticker=` added to your URL's query string. If you already handle `/create/` callbacks, there is nothing new to write. - **Link paid** (`payment` links only): one extra callback when the link becomes paid, to your URL as given. It is retried until 3 days after the link was paid. | Field | Description | | ------------------ | ----------------------------------------------------------------- | | `type` | `payment` | | `link_id` | The link | | `value` | The amount asked | | `currency` | Its fiat currency, such as `USD` | | `paid_amount_fiat` | The confirmed total in that currency, above `value` when overpaid | | `paid_coin` | Coin paid, in underscore form such as `trc20_usdt` | | `paid_amount` | Amount paid in that coin | | `exchange_rate` | Fiat per coin, averaged over the deposits in that coin | | `txids` | Every transaction counted, comma-separated | | `address_in` | The address paid | | `is_paid` | `1` | A link paid in several coins lists `paid_coin`, `paid_amount`, `exchange_rate` and `address_in` once per coin, comma-separated, in the order they were first paid. ## Partial, late and extra payments Each payment is forwarded as soon as it confirms; nothing waits for the total. - **Partial**: the link stays open and the page asks for what is left, quoted again at the current rate. - **Overpaid**: the surplus is forwarded with the rest, and `paid_amount_fiat` shows it. There are no refunds: NoncePay holds nothing to refund from. - **After the quote expired**: the payment counts at the current price. - **After the link expired or was disabled**: the payment is still forwarded and reported by its own callback, but it does not change the link and no link-paid callback is sent. - **Below the coin's minimum**: recorded, never forwarded, and it does not count towards the link. - **Tolerance**: with `tolerance=0.5`, a link of 100 USD counts as paid from 99.50 USD, which covers an exchange that deducts its withdrawal fee. ## Limits and abuse - `/link/create/` allows 30 links a minute and 1,000 a day per IP. - Anyone can create a link, so NoncePay does not vouch for who is behind one: the page says so, and shows the host of `redirect_url` before paying. Description and merchant name are plain text. - The page has a **Report this link** button ([`/link/{link_id}/report/`](/docs/api/payment-links/report-link)). A link reported for phishing or a scam can be disabled by NoncePay. Disabling is final: the page stops offering a way to pay, and payments already on their way are still forwarded. # 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.noncepay.app/{$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" Module Manager**, click **Upload a module** and drop the zip. 3. Click **Configure**, enter a receiving wallet for each family of coins you accept, then save. Requires PrestaShop 8.x (1.7.8 also works) and PHP 7.4 to 8.3 with `curl` and `openssl`. Your store must be reachable from the internet and not in maintenance mode so NoncePay can call its callback. The module is in English and French. The source is on [GitHub](https://github.com/Nonce-pay/noncepay-prestashop). ## How it works 1. At checkout the customer picks **Pay with crypto** and places the order, which is created in the **Awaiting NoncePay payment** status. 2. The module creates a [payment link](/docs/guides/payment-links) for the order total in the order currency, valid 7 days, and redirects the customer to its page. Coming back from the order confirmation page or the order history reuses the same link; a new one is created only when the total changes or the link is about to expire. 3. The customer picks a coin and pays. Each payment is forwarded to your wallet as soon as it confirms. 4. Once the payments cover the order, NoncePay sends the link-paid [callback](/docs/callbacks). The module verifies its `x-ca-signature` against the key from [`/pubkey/`](/docs/api/utilities/get-public-key), checks the link and the amount, records the payment with the link id as transaction id and moves the order to **Payment accepted**, so a repeated callback is recorded once. 5. The customer is sent back to the order confirmation page, which says the payment is being confirmed. The order is only marked paid by the signed callback, never by this redirect. Callbacks are listed in **Advanced Parameters > Logs**, attached to their order. ## Settings | Setting | Default | What it does | | ---------------------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | Wallets | empty | One per family: EVM, Tron, Bitcoin, Litecoin, Bitcoin Cash, Dogecoin, Solana; split syntax accepted | | Coins offered | every coin of your wallets | Optional comma-separated tickers such as `trc20/usdt,bep20/usdt,btc` | | Merchant name, logo | shop name, none | Shown on the payment page; the logo must be an `https` URL | | Underpayment tolerance | 0% | 0 to 1%: how much less than the order total still marks it paid | | Pinned public key | empty | Optional PEM key; empty fetches `/pubkey/` and caches it | ## Limits - The cart currency must be one NoncePay prices (USD, EUR, GBP and about thirty others). For another currency the option is hidden, and the configuration page lists the currencies concerned. - An overpayment is recorded in full as the order payment. An underpayment beyond the tolerance is asked for again on the payment page, and the order keeps waiting. - A payment made after the link expired is still forwarded to your wallet and shows in the logs, but it does not mark the order paid: add it by hand on the order page. - Refunds are sent from your wallet: NoncePay holds nothing to refund from. # Accept crypto payments with Python This guide uses `requests`, `flask` and `cryptography`: ```bash pip install requests flask cryptography ``` ## Create a payment address ```python title="create_address.py" import requests API = "https://api.noncepay.app" def create_address(ticker: str, order_id: int, wallet: str) -> 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.noncepay.app/pubkey/", timeout=15).json()["pubkey"].encode() ) app = Flask(__name__) def is_signed_by_noncepay(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_noncepay(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. Runnable shops with this flow: [Flask](https://github.com/Nonce-pay/noncepay-examples/tree/main/python/flask) and [FastAPI](https://github.com/Nonce-pay/noncepay-examples/tree/main/python/fastapi). # Accept crypto payments with WHMCS The NoncePay module for WHMCS adds a **Pay with crypto** button to your invoices. The customer pays on the NoncePay hosted payment page and the invoice is marked paid once the payment is confirmed. Every payment is forwarded to your own wallet, minus our [fee](/docs/fees). There is no account, no API key and no withdrawal step. ## Install 1. Download `noncepay-whmcs.zip` from the [latest release](https://github.com/Nonce-pay/noncepay-whmcs/releases/latest) and unzip it into the root of your WHMCS installation. It adds `modules/gateways/noncepay.php`, `modules/gateways/noncepay/` and `modules/gateways/callback/noncepay.php`. 2. In the WHMCS admin, go to **Configuration > System Settings > Payment Gateways**, open **All Payment Gateways** and click **NoncePay (crypto)**. 3. Enter a receiving wallet for each family of coins you accept, then save. Requires WHMCS 8.x and PHP 7.4 to 8.3 with `curl` and `openssl`. Your WHMCS site must be reachable from the internet so NoncePay can call its callback. The source is on [GitHub](https://github.com/Nonce-pay/noncepay-whmcs). ## How it works 1. The invoice shows **Pay with crypto**. Nothing is sent to NoncePay until the customer clicks it. 2. The module creates a [payment link](/docs/guides/payment-links) for the invoice balance in the invoice currency, valid 7 days, and redirects the customer to its page. A reload reuses the same link; a new one is created only when the balance changes or the link is about to expire. 3. The customer picks a coin and pays. Each payment is forwarded to your wallet as soon as it confirms. 4. Once the payments cover the invoice, NoncePay sends the link-paid [callback](/docs/callbacks). The module verifies its `x-ca-signature` against the key from [`/pubkey/`](/docs/api/utilities/get-public-key), checks the link and the amount, and records the payment with the link id as transaction id, so a repeated callback is recorded once. 5. The customer is sent back to the invoice, which says the payment is being confirmed. The invoice is only marked paid by the signed callback, never by this redirect. Callbacks are listed in **Billing > Gateway Log**. ## Settings | Setting | Default | What it does | | ---------------------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | Wallets | empty | One per family: EVM, Tron, Bitcoin, Litecoin, Bitcoin Cash, Dogecoin, Solana; split syntax accepted | | Coins offered | every coin of your wallets | Optional comma-separated tickers such as `trc20/usdt,bep20/usdt,btc` | | Merchant name, logo | company name, none | Shown on the payment page; the logo must be an `https` URL | | Underpayment tolerance | 0% | 0 to 1%: how much less than the invoice still marks it paid | | Pinned public key | empty | Optional PEM key; empty fetches `/pubkey/` and caches it | ## Limits - The invoice currency must be one NoncePay prices (USD, EUR, GBP and about thirty others). For another currency, set **Convert To For Processing** on the gateway to one of them. - An overpayment is recorded in full and WHMCS adds the surplus to the client's credit. An underpayment beyond the tolerance is asked for again on the payment page. - A payment made after the link expired is still forwarded to your wallet and shows in the gateway log, but it does not mark the invoice paid: add it by hand. - Refunds are sent from your wallet: NoncePay holds nothing to refund from. # Accept crypto payments with WooCommerce The NoncePay plugin for WooCommerce adds a crypto payment method to your store. Customers pick a coin at checkout, see the exact amount, a QR code and a payment address, and the order updates by itself once the payment is confirmed. Every payment is forwarded to your own wallet, minus 1.25%. There is no account, no API key and no withdrawal step. ## Install 1. In WordPress, go to **Plugins > Add New**, search for **NoncePay**, then install and activate [NoncePay – Crypto Payment Gateway for WooCommerce](https://wordpress.org/plugins/noncepay-for-woocommerce/). 2. Go to **WooCommerce > Settings > Payments > NoncePay**, enable it and enter a receiving wallet for each coin you want to accept. Only coins with a wallet are offered at checkout. Requires WordPress 6.4+, WooCommerce 8.0+ and PHP 7.4+. The plugin is translated into 12 languages. Without access to the plugin directory, download `noncepay-for-woocommerce.zip` from the [latest release](https://github.com/Nonce-pay/noncepay-woocommerce/releases/latest) and upload it in **Plugins > Add New > Upload Plugin**. The source is on [GitHub](https://github.com/Nonce-pay/noncepay-woocommerce). ## What it does - **Coins:** USDT on Tron, BNB Smart Chain and Ethereum; USDC on Base, Polygon, Arbitrum and Solana; BTC, ETH, LTC, TRX, SOL, BNB, DOGE, and every other coin from [`/info/`](/docs/api/coins/get-all-info). - **Checkout:** works with the classic checkout and the Cart and Checkout blocks, and with High-Performance Order Storage. - **Payment page:** amount and address with copy buttons, QR code, a warning to send on the right network, a quote countdown, and a live status that moves from waiting to payment seen to confirmed. - **Order status:** the order goes on hold as soon as the payment is seen on chain, and to processing (or completed) once it is forwarded to your wallet. Each step adds an order note with the transaction ids. - **Edge cases:** - Partial payments add up. - An underpayment beyond your tolerance keeps the order on hold. - A payment made after the quote expired is checked against the current rate before the order is marked paid. - A payment to a cancelled order goes on hold for review. - **Security:** every callback is verified with the [RSA signature](/docs/callbacks) and a per-order secret, and processed once even when callbacks arrive together. Amounts use exact decimal math. ## Settings | Setting | Default | What it does | | ---------------------- | ---------- | ----------------------------------------------------------- | | Wallets | empty | Your receiving address per coin; a coin is offered when set | | Underpayment tolerance | 0.5% | How much less than the quote still marks the order paid | | Quote validity | 30 minutes | How long the converted amount is honoured | | Paid status | processing | Status given to a paid order | | Debug log | off | Writes to WooCommerce > Status > Logs | Your store currency must be one NoncePay converts (USD, EUR, GBP and about thirty others); the settings page warns you when it is not. ## Behind a proxy or CDN The plugin checks each callback's signature against the URL it registered for the order, so it keeps working with a CDN in front of WordPress. A proxy that rewrites the host or path before WordPress sees it needs the `noncepay_callback_base_url` filter: return the public scheme and host of your site. Not on WooCommerce? The [PHP guide](/docs/guides/php) and the [runnable examples](https://github.com/Nonce-pay/noncepay-examples) show the same flow in plain PHP and Laravel. # Introduction NoncePay 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.noncepay.app` - 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.noncepay.app/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.noncepay.app/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.