TapCoin Pay API
Create checkouts, payment links and invoices from your own code, and get a signed webhook when the chain confirms a payment. Base URL https://tapcoinpay.com. All responses are JSON in a { ok, data } envelope.
Quickstart
Three steps from zero to a confirmed payment. About ten minutes.
1. Create an API key
In the dashboard go to Developers and create a key. Keys start with tcp_live_ or tcp_test_. The raw key is shown once; store it as a secret in your environment. Test keys create checkouts on test networks and never match real funds.
2. Create a checkout
Amounts are integer minor units of the fiat currency: 25000 is $250.00. Pass an Idempotency-Key header so a retried request returns the same checkout instead of creating a second one (keys are honored for 24 hours).
curl https://tapcoinpay.com/v1/checkouts \ -H "Authorization: Bearer tcp_live_…" \ -H "Idempotency-Key: order_1043" \ -H "Content-Type: application/json" \ -d '{ "amount": 25000, "currency": "USD", "description": "Order 1043", "assets": ["XRP","BTC","ETH","USDC"], "redirect_url": "https://example.com/thanks", "customer_email": "sam@example.com", "metadata": { "order_id": "1043" } }'
Response:
{ "ok": true, "data": {
"id": "co_3kD9pQx7", "object": "checkout", "status": "created",
"amount": 25000, "currency": "USD", "description": "Order 1043",
"assets": ["XRP","BTC","ETH","USDC"],
"checkout_url": "https://tapcoinpay.com/pay/co_3kD9pQx7",
"qr_url": "https://tapcoinpay.com/qr/co_3kD9pQx7.svg",
"expires_at": "2026-09-13T18:45:00Z", "created_at": "2026-09-13T18:30:00Z",
"metadata": { "order_id": "1043" }
} }
Send the customer to checkout_url. They pick an asset, see the exact amount and address, and pay from their own wallet.
3. Handle the webhook
Register an endpoint under Developers › Webhooks. You receive payment.detected when the transaction appears on the network and payment.confirmed when it reaches the required confirmations. Fulfil the order on payment.confirmed, never on detected. Verify the signature first (see Webhooks).
// Express app.post("/webhooks/tapcoin", express.raw({ type: "*/*" }), (req, res) => { if (!verify(req.body.toString(), req.get("TapCoin-Signature"), process.env.TAPCOIN_WEBHOOK_SECRET)) return res.status(400).end(); const evt = JSON.parse(req.body); if (evt.event === "payment.confirmed") markOrderPaid(evt.data.checkout.metadata.order_id); res.status(200).end(); });
Authentication
Send your key as a bearer token on every request: Authorization: Bearer tcp_live_…. Keys belong to one organization and carry a mode (live or test). Rotate a key from the dashboard at any time; the old key stops working immediately.
Never put a live key in browser code. For browser-side buttons use the embed script, which needs only a public button id.
API reference
All endpoints are under /v1. List endpoints accept ?limit= (max 100) and return next_cursor when there are more results.
| Method | Path | Description |
|---|---|---|
POST | /v1/checkouts | Create a checkout. Body: amount, currency, description?, assets?, redirect_url?, customer_email?, metadata?. Honors Idempotency-Key. |
GET | /v1/checkouts/:id | Retrieve a checkout, including its current status and the payment if one was detected. |
GET | /v1/checkouts?limit= | List checkouts, newest first. |
POST | /v1/payment_links | Create a reusable link. Body: title, slug?, amount? (omit to let the customer enter one), currency?, description?, assets, redirect_url?. |
GET | /v1/payment_links | List payment links. |
POST | /v1/invoices | Create an invoice and its checkout. Body: amount, currency?, customer_email?, description?, line_items?, due_date?, assets. |
GET | /v1/invoices/:id | Retrieve an invoice with its checkout and payment status. |
GET | /v1/payments | List payments (confirmed, detected and underpaid), newest first. |
GET | /v1/payments/:id | Retrieve a payment with transaction hash, confirmations and the amount received. |
GET | /v1/assets | Supported assets and which ones your organization has a wallet for. |
GET | /v1/quotes?asset=XRP¤cy=USD | Current price used for conversion. Informational; checkouts lock their own quote. |
POST | /v1/webhooks/verify | Helper: send { payload, signature } and get { valid }. Useful in languages without a convenient HMAC. |
Objects and statuses
A checkout is one attempt to collect one amount. It moves through these statuses:
createdCheckout exists, no asset chosen yet.awaiting_paymentAsset chosen, address and exact amount issued, quote locked for 15 minutes.detectedA matching transaction is on the network but below the required confirmations.confirmedRequired confirmations reached. Fulfil the order.underpaidA transaction arrived for less than the amount due.expiredNo payment before the quote expired. The customer can start again.A payment is the on-chain transaction that satisfied a checkout: id, checkout_id, asset, amount_crypto (string, minor units), tx_hash, confirmations, from_address?, detected_at, confirmed_at?. Crypto amounts are strings of integer minor units (drops, satoshis, wei, USDC base units) so nothing is lost to floating point.
Webhooks
TapCoin Pay POSTs a JSON body to your endpoint for every event. Deliveries are retried with backoff for 24 hours until your endpoint returns a 2xx. Events can arrive out of order and more than once; use id to deduplicate.
Events
payment.created, payment.detected, payment.confirmed, payment.underpaid, payment.expired, invoice.created, invoice.paid, invoice.expired.
Payload
{ "id": "evt_9Hq2…", "event": "payment.confirmed", "created_at": "2026-09-13T18:41:07Z",
"data": { "checkout": { … }, "payment": { … }, "invoice": { … } } }
Signature
Every delivery carries a TapCoin-Signature header:
TapCoin-Signature: t=1757789467,v1=5f8c0e…9a1d
To verify: take the raw request body exactly as received, build the string "<t>.<rawBody>", compute HMAC-SHA256 with your endpoint secret, hex-encode it, and compare to v1 with a constant-time comparison. Reject deliveries where t is more than five minutes from your clock.
import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map(p => p.split("="))); const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; return fresh && expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); }
# PHP [$t, $v1] = array_map(fn($p) => explode('=', $p, 2)[1], explode(',', $_SERVER['HTTP_TAPCOIN_SIGNATURE'])); $expected = hash_hmac('sha256', $t . '.' . file_get_contents('php://input'), $secret); $valid = abs(time() - (int)$t) < 300 && hash_equals($expected, $v1);
Use the Send test event button on a webhook in the dashboard to check your endpoint before going live.
Embed script
The embed script adds a Pay with Crypto button to any page without server code. It opens the hosted checkout in a centered modal on desktop and a new tab on mobile.
<script src="https://tapcoinpay.com/js/tapcoin.js" defer></script> <!-- a pay button created in the dashboard (fixed amount) --> <button data-tapcoin-button="btn_8f2k1q">Pay with Crypto</button> <!-- or open a checkout you created server-side --> <button data-tapcoin-checkout="co_3kD9pQx7">Pay $250.00 in crypto</button>
You can also open a checkout from JavaScript:
window.TapCoin.open({ checkoutId: "co_3kD9pQx7" }); // or { buttonId: "btn_8f2k1q" }
When the payment is confirmed the modal shows a receipt and, if the checkout has a redirect_url, sends the customer there. A button link without JavaScript also works: https://tapcoinpay.com/js/button/btn_8f2k1q creates a checkout and redirects to it.
WordPress and WooCommerce plugin
- In WordPress go to Plugins › Add New, search for TapCoin Pay and install it, or upload
tapcoin-pay.zipfrom your dashboard. - Activate the plugin, then open WooCommerce › Settings › Payments › TapCoin Pay.
- Paste a live API key and the webhook secret the plugin shows you. The plugin registers its own webhook URL (
/wc-api/tapcoin) with TapCoin Pay automatically. - Choose which assets to offer and the button label. Save.
- Place a test order. The order moves to On hold when a payment is detected and Processing when it is confirmed. Underpaid and expired checkouts add an order note.
Requires WordPress 6.2 or newer, WooCommerce 8 or newer and PHP 8.1 or newer. The plugin never stores wallet keys; it only calls the TapCoin Pay API with your key.
Supported assets
Each asset is matched to a checkout differently, because each network offers different tools. The customer never sees this; they see one address and one amount.
| Asset | Network | Matching strategy | Required confirmations | Typical time |
|---|---|---|---|---|
| XRP | XRP Ledger | One receiving address plus a unique destination tag per checkout. The QR and the payment URI include the tag. Payments without a tag are flagged for manual matching. | 1 | 3 to 5 seconds |
| BTC | Bitcoin | A fresh address per checkout, derived from your xpub or zpub (public key only). If you added a single address instead, checkouts are matched by a unique amount (a few extra satoshis). | 1 | about 10 minutes |
| ETH | Ethereum mainnet | Unique amount per checkout on your single address (a few extra wei), matched against incoming value transfers. | 6 | about 1.5 minutes |
| USDC | Ethereum mainnet (ERC-20) | Unique amount per checkout on your single address, matched against Transfer events to your address. | 6 | about 1.5 minutes |
Quotes are locked for 15 minutes from asset selection. Payments that arrive after expiry are still detected and shown in your dashboard for manual reconciliation; they are not auto-confirmed against the expired checkout.
Errors and limits
Errors use the same envelope: { "ok": false, "error": { "code", "message" } } with an HTTP status of 400 (validation), 401 (bad key), 402 (plan limit reached), 404, 409 (idempotency conflict) or 429 (rate limit). Live keys are limited to 120 requests a minute; list endpoints to 30. Hosted checkout status polling is rate-limited per session on the server, so your customers never need to worry about it.
Questions or something missing? Email dev@tapcoinpay.com or open an issue on GitHub.