# A crypto payment API that behaves like the rest of your stack

> Create an invoice, show the customer a one-time deposit address, and let signed webhooks tell your backend when the money is confirmed. No nodes to run, no chain indexers to write — just a REST API with documented failure states.

Updated 2026-09-30 · Payaries · https://www.payaries.com/crypto-payment-api

## What the API does

Payaries is a multi-chain crypto payment gateway. Your backend talks to it over HTTPS; your customers never see it. The surface a payment integration needs is small:

- `POST /v1/invoices` — creates an invoice for a fiat amount in a chain and asset you choose, locks the exchange rate, and returns a one-time deposit address.
- `GET /v1/invoices/:id` — the invoice's current status, [confirmations](https://www.payaries.com/guides/crypto-payment-confirmations) and, once forwarding starts, its payout.
- Webhooks — a signed `invoice.status_changed` event on every transition, and `withdrawal.status_changed` for payouts.
- `POST /v1/withdrawals` — sends funds from your own wallet to any address you name.
- `GET /v1/chains` and `GET /v1/rates/:asset/:fiat` — public endpoints for the live chain and asset list and informational prices.

The whole contract is published as an OpenAPI document at `/openapi.json`, with a stable `operationId` on every operation. Building with an AI coding assistant? Start at [Payaries for AI agents](https://www.payaries.com/ai-agents).

## Authentication: signed requests

Server-to-server calls carry an API key and an [HMAC](https://www.payaries.com/guides/sign-crypto-payment-api-requests)-SHA256 signature over the timestamp, HTTP method, request target (path plus query string) and body. The secret never leaves your server.

**Signing a request (Node)**

```
const crypto = require('crypto');

const timestamp = Date.now().toString();
const body = JSON.stringify(requestBody ?? {});
const message = `${timestamp}\n${method.toUpperCase()}\n${requestTarget}\n${body}`;
const signature = crypto.createHmac('sha256', apiSecret).update(message).digest('hex');
```

- Requests older than five minutes are rejected, so keep your server clock in sync.
- Each signature is single-use for state-changing requests (`POST`, `PATCH`, `DELETE`). A retry needs a fresh timestamp and a new signature.
- API keys can be restricted to a list of IP addresses.
- Managing API keys, rotating the webhook secret, changing the webhook URL or redirect domains and approving withdrawals are dashboard-session-only actions — a leaked API key can't do any of them.

> **Never put the API secret in browser code.** Call the API from your server and expose only what your page needs. Test and live keys are separate, and you can hold several of each — one per integration or environment.

## Create an invoice

`referenceId` is your idempotency key: replaying the same value for your account returns the existing invoice instead of creating a duplicate, so a network retry can never double-bill.

**Request**

```
POST /v1/invoices
X-Api-Key: <your api key>
X-Timestamp: 1725700000000
X-Signature: <hex HMAC-SHA256>

{
  "referenceId": "order_10231",
  "fiat": { "amount": "49.99", "currency": "USD" },
  "crypto": { "asset": "USDT_TRC20", "chainId": "tron-mainnet" },
  "metadata": { "customerId": "cus_123" }
}
```

**Response**

```
201 Created
{
  "referenceId": "order_10231",
  "crypto": { "asset": "USDT_TRC20", "chainId": "tron-mainnet", "amountDue": "49.99", "rateLocked": "1" },
  "depositAddress": { "value": "TXyz..." },
  "status": "pending",
  "confirmations": { "required": 19, "current": 0 },
  "expiresAt": "2026-09-08T00:30:00.000Z"
}
```

Show `depositAddress.value` to your customer (as text and a QR code). The fiat amount is converted at creation and the rate stays locked for that invoice.

## The invoice lifecycle

An invoice moves through `pending` → `detected` → `confirming` → `confirmed`, then `forwarding` → `settled` as the deposit is swept to your wallet. Your ledger is credited the moment it reaches `confirmed`; that credit becomes withdrawable once the sweep settles.

`expired`, `underpaid` and `failed` are the off-ramps. `expired` is final unless you later get a transition out of it: a payment made on time but noticed late moves it to `detected` or `underpaid`, while one mined after the deadline is held for review (see below). See [handling underpaid and late payments](https://www.payaries.com/guides/handle-underpaid-and-late-crypto-payments).

## Webhooks

Every status change is delivered as a signed POST. Verify the `X-Gateway-Signature` header — an HMAC-SHA256 of the timestamp and the raw body — before you trust it. Failed deliveries are retried immediately, then after 1 minute, 5 minutes, 15 minutes, 1 hour and 6 hours.

**Verifying (Node)**

```
function verifyWebhook(rawBody, signature, timestamp, webhookSecret, toleranceMs = 5 * 60 * 1000) {
  if (Math.abs(Date.now() - timestamp) > toleranceMs) return false;
  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(timestamp + rawBody)
    .digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(signature, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

The full walkthrough, including dedupe and ordering, is in [verifying crypto webhook signatures](https://www.payaries.com/guides/verify-crypto-webhook-signatures).

## Withdrawals

Withdraw to any address, not just a pre-registered one. With an API key the `Idempotency-Key` header is required: the same key with the same parameters returns the original withdrawal, and the same key with different parameters is a `409`. Withdrawals created by an API key execute immediately below your auto-approve threshold and wait for your approval in the dashboard above it.

Withdrawals are sent from your own wallet, which needs a little of the chain's native currency (ETH, BNB, POL or TRX, for example) to pay network gas on chains where Payaries's [gas station](https://www.payaries.com/blog/gas-station-network-fees) doesn't cover it — it isn't topped up for you. Where the station covers a chain, the gas is recovered as a network fee inside the transaction instead.

## Supported networks

| Network | Assets | Deposits | Withdrawals |
|---|---|---|---|
| Ethereum mainnet | ETH, USDT (ERC-20), USDC (ERC-20), DAI, WBTC and more | Yes | Yes |
| BNB Smart Chain | BNB, USDT (BEP-20), USDC (BEP-20), DAI and more | Yes | Yes |
| Polygon | POL, USDT, USDC, DAI, WBTC | Yes | Yes |
| Arbitrum | ETH, USDT, USDC, DAI, WBTC | Yes | Yes |
| Tron mainnet | TRX, USDT (TRC-20) | Yes | Yes |
| Bitcoin | BTC | Yes | Not yet |

More networks and assets are being added. The authoritative list is always the public `GET /v1/chains` endpoint — an asset only appears there once it is actually configured.

## Errors, limits and what to know first

- Errors are plain JSON (`{ "error": "…" }`) with conventional status codes: `400` validation, `401` auth or replayed signature, `403` forbidden, `404` not found, `409` conflict, `429` rate limited, `503` a security dependency is temporarily unavailable — retry with a fresh signature.
- Rate limits: 120 requests a minute per IP overall, and 20 a minute for creating invoices or withdrawals.
- Payaries runs on real mainnet networks only. There is no sandbox — test-mode API keys transact on the same chains as live keys, so integration testing uses a small real payment. Live keys are issued once your account is approved.
- Crypto transactions are irreversible once confirmed. A refund to a customer is a separate transaction you send yourself.

## Questions

**Is there a sandbox or testnet for the crypto payment API?**
No. Payaries runs on real mainnet networks only. Test-mode API keys work immediately and transact on the same real chains as live keys; "live" only means your account has been approved. Test an integration with a small real payment.

**How do I authenticate requests to the API?**
Send your API key in X-Api-Key, the current time in X-Timestamp and an HMAC-SHA256 signature in X-Signature. The signature covers the timestamp, HTTP method, request path and query string, and body, and is computed with your API secret.

**How do I avoid creating duplicate invoices or withdrawals?**
Invoices use your referenceId as the idempotency key — the same value returns the existing invoice. Withdrawals made with an API key require an Idempotency-Key header; the same key with the same parameters returns the original withdrawal.

**What happens if my webhook endpoint is down?**
Deliveries are retried on a schedule that spans six hours: immediately, then after 1 minute, 5 minutes, 15 minutes, 1 hour and 6 hours. After the final failure an invoice's webhookStatus becomes failed, and you can re-send it with POST /v1/invoices/:id/resend-webhook.

**Which blockchains does the crypto payment API support?**
Ethereum, BNB Smart Chain, Polygon, Arbitrum and Tron support deposits and on-chain withdrawals, and more chains are on the way. Bitcoin supports deposits only for now. Call GET /v1/chains for the live list.

## Try it

Create a test API key and run a real invoice: [Payaries — www.payaries.com](https://www.payaries.com/signup). Payaries is a multi-chain crypto payment gateway — https://www.payaries.com

## Keep reading

- https://www.payaries.com/guides/crypto-payment-api-quickstart — Crypto payment API quickstart: from signup to your first payout
- https://www.payaries.com/guides/sign-crypto-payment-api-requests — How to sign crypto payment API requests (HMAC-SHA256)
- https://www.payaries.com/ai-agents — Build a crypto payment integration with an AI agent, safely
- https://www.payaries.com/guides/verify-crypto-webhook-signatures — How to verify crypto payment webhook signatures
- https://www.payaries.com/guides/how-a-crypto-payment-gateway-works — How a crypto payment gateway works, from invoice to settlement
- https://www.payaries.com/usdt-payment-gateway — Accept USDT payments on Ethereum, BNB Smart Chain, Polygon, Arbitrum, Tron and more
- https://www.payaries.com/custom-token-payments — Custom token payments: bring your own ERC-20, BEP-20 or TRC-20 token
