One API, one hosted flow
CryptoPayIn prices an order in your selected presentment currency, locks verified fiat/USD and crypto/USD snapshots, allocates a dedicated deposit address and watches its own blockchain nodes for payment. Internal accounting, fees and balances remain in USD. Your backend receives a checkout URL immediately and signed lifecycle events afterward.
This is a live API. There is no sandbox prefix. Every successful creation allocates a real on-chain address. Use a small supported-currency amount for end-to-end tests and keep secret keys on your server.
How the integration fits together
Generate a secret once in Dashboard -> Developers.
POST the order currency, amount and selected asset.
Send the customer to the returned hosted URL.
Verify the HMAC and update your order idempotently.
Create your first payment
Generate an API key in the merchant dashboard, store the secret in an environment variable, then create a payment from your backend. The example uses ETH so it can be tested without choosing a token network.
curl --request POST https://cryptopayin.com/v1/payments \
--header "Authorization: Bearer $CPI_SECRET_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: order_1042" \
--data '{
"amount": 49.99,
"currency": "USD",
"asset": "ETH",
"order_ref": "order_1042",
"redirect_url": "https://shop.example/orders/1042/paid"
}'
const response = await fetch("https://cryptopayin.com/v1/payments", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.CPI_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "order_1042"
},
body: JSON.stringify({
amount: 49.99, currency: "USD", asset: "ETH",
order_ref: "order_1042",
redirect_url: "https://shop.example/orders/1042/paid"
})
});
if (!response.ok) throw new Error(await response.text());
const payment = await response.json();
$payload = json_encode([
'amount' => 49.99, 'currency' => 'USD', 'asset' => 'ETH',
'order_ref' => 'order_1042',
'redirect_url' => 'https://shop.example/orders/1042/paid',
]);
$ch = curl_init('https://cryptopayin.com/v1/payments');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('CPI_SECRET_KEY'),
'Content-Type: application/json',
'Idempotency-Key: order_1042',
],
]);
$payment = json_decode(curl_exec($ch), true, flags: JSON_THROW_ON_ERROR);
import os, requests
response = requests.post(
"https://cryptopayin.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['CPI_SECRET_KEY']}",
"Idempotency-Key": "order_1042",
},
json={
"amount": 49.99, "currency": "USD", "asset": "ETH",
"order_ref": "order_1042",
"redirect_url": "https://shop.example/orders/1042/paid",
}, timeout=15,
)
response.raise_for_status()
payment = response.json()Use the response
Persist the payment id beside your order, then redirect the customer to checkout_url. Do not calculate a crypto amount or deposit address yourself.
{
"id": "P-9F27C1E4KD",
"object": "payment",
"status": "created",
"amount": 49.99,
"amount_decimal": "49.99",
"amount_minor": 4999,
"currency": "USD",
"currency_minor_units": 2,
"amount_usd": 49.99,
"amount_usd_cents": 4999,
"fx_rate_usd": "1.000000000000",
"fx_source": "fixed:USD",
"fx_observed_at": "2026-07-17T13:00:00+00:00",
"fx_discrepancy_bps": 0,
"asset": "ETH",
"network": "mainnet",
"crypto_amount": "0.01388612",
"crypto_received": "0",
"deposit_address": "0x71b8c3d4700000000000000000000000000084e2",
"exchange_rate": "3600.00000000",
"exchange_rate_currency": "USD",
"exchange_rate_source": "median:cb,cg,cl",
"exchange_rate_source_count": 3,
"exchange_rate_observed_at": "2026-07-17T13:00:00+00:00",
"exchange_rate_discrepancy_bps": 12,
"confirmations": 0,
"confirmations_required": 12,
"order_ref": "order_1042",
"checkout_url": "https://cryptopayin.com/i/P-9F27C1E4KD",
"expires_at": "2026-07-17T13:30:00+00:00",
"created_at": "2026-07-17T13:00:00+00:00",
"completed_at": null
}Authentication
Every API request uses the secret key in an HTTP Bearer header. Secret keys begin with csk_live_. The companion cpk_live_ value is a public identifier for your dashboard and must not be used as the Bearer credential.
Authorization: Bearer csk_live_...Every endpointThe raw secret is returned only when the key is created. CryptoPayIn stores a password hash plus an indexed SHA-256 lookup, never the plaintext secret.
Create separate keys per application or environment and revoke them independently. An account can hold up to 50 active keys.
Never put a csk_live_ value in browser JavaScript, a mobile binary, a public repository or a checkout page.
A webhook endpoint can be account-wide or attached to one API key, keeping integrations isolated.
A missing, malformed, revoked or unknown secret returns 401 unauthorized. A suspended or closed merchant account is rejected the same way. A valid key used outside its granted permissions returns 403 insufficient_scope.
Idempotency
Send a unique Idempotency-Key on every payment creation. If your connection drops after submission, retry the identical JSON with the identical key: CryptoPayIn returns the original payment instead of allocating another address.
| Case | Result | HTTP |
|---|---|---|
| First use | Creates and returns a new payment. | 201 |
| Same key + same JSON | Returns the existing payment with Idempotent-Replayed: true. | 200 |
| Same key + different JSON | Rejects the request as idempotency_conflict. | 409 |
Keys are scoped to the API credential and may contain 1-128 letters, digits, dots, underscores, colons or dashes. A durable order UUID is a good choice. The same mechanism also protects withdrawals, so a retried payout can never move funds twice.
API reference
The API covers payments, payment links, shops, products, balances and withdrawals. Responses use UTF-8 JSON over HTTPS; any create or update requires Content-Type: application/json. Mutations are gated by the calling key's permissions. There is intentionally no browser CORS workflow: calls belong on your backend.
/v1/assetsDiscover available assets/v1/currenciesDiscover fiat presentment currencies/v1/paymentsCreate a payment/v1/paymentsList and filter payments/v1/payments/{id}Retrieve a payment/v1/payments/{id}/cancelCancel an unfunded paymentVersion 1 may receive backward-compatible fields and endpoints. A breaking contract change will use a new base path rather than silently changing /v1.
List assets
/v1/assetsBearer auth requiredUse this endpoint as the source of truth for checkout choices. It returns enabled catalog entries, current independently checked rates, minimum USD-equivalent amounts and whether the node plus price feed are ready. An entry can remain listed with available: false while a node is synchronizing or its price cannot be verified.
curl https://cryptopayin.com/v1/assets \
--header "Authorization: Bearer $CPI_SECRET_KEY"{
"object": "list",
"data": [{
"asset": "USDT",
"network": "TRC20",
"type": "token",
"decimals": 6,
"minimum_amount": 1,
"currency": "USD",
"base_confirmations": 19,
"available": true,
"rate_usd": "1.00000000",
"rate_source": "median:cb,cg,cl",
"rate_source_count": 3,
"rate_discrepancy_bps": 4,
"rate_status": "healthy",
"rate_updated_at": "2026-07-17T13:00:00+00:00"
}]
}Catalog and network identifiers
| Asset | Network value | Shorthand | Base confirmations |
|---|---|---|---|
| BTC | mainnet | BTC | 2 |
| ETH | mainnet | ETH | 6 |
| USDT | TRC20 / ERC20 | USDT.TRC20 | 19 / 6 |
| USDC | ERC20 | USDC.ERC20 | 6 |
| DAI | ERC20 | DAI | 6 |
| SHIB | ERC20 | SHIB | 6 |
| PEPE | ERC20 | PEPE | 6 |
| LTC | mainnet | LTC | 6 |
| TRX | mainnet | TRX | 19 |
| DOGE | mainnet | DOGE | 20 |
| XMR | mainnet | XMR | 10 |
| SOL | mainnet | SOL | 32 |
Availability is dynamic. Do not hardcode the table above as a live allow-list. For multi-network symbols such as USDT, send network explicitly or use the ASSET.NETWORK shorthand.
List fiat currencies
/v1/currenciesBearer auth requiredReturns the enabled presentment currencies, their ISO precision and current USD conversion health. Only offer rows with available: true. USD is intrinsic; every other currency needs a fresh live quote and an independent reference check. minimum_amount converts the lowest configured enabled-asset floor into that currency; the chosen asset can require a higher amount, so always read GET /v1/assets too.
curl https://cryptopayin.com/v1/currencies \
--header "Authorization: Bearer $CPI_SECRET_KEY"{
"object": "list",
"accounting_currency": "USD",
"data": [{
"currency": "EUR",
"name": "Euro",
"symbol": "€",
"minor_units": 2,
"available": true,
"minimum_amount": "0.86",
"rate_usd": "1.160000000000",
"rate_source": "coinbase+ecb",
"rate_source_count": 2,
"rate_discrepancy_bps": 18,
"rate_updated_at": "2026-07-17T13:00:00+00:00",
"rate_reference_at": "2026-07-16T00:00:00+00:00"
}]
}Currency omitted on POST /v1/payments still means USD for backward compatibility. Zero-decimal currencies such as JPY reject fractional amounts. Treat catalogue minimums and rates as live data, never hardcoded constants.
Create a payment
/v1/payments120 requests / minute / keyCreates an invoice in the requested presentment currency, locks fresh verified fiat/USD and crypto/USD snapshots, computes the exact crypto amount and binds a dedicated on-chain deposit address.
Request body
| Field | Type | Requirement | Description |
|---|---|---|---|
| amount | number or decimal string | required | Value in currency, at that currency's ISO precision. Its locked USD equivalent must not exceed $1,000,000.00; asset minimums also apply. |
| currency | string | optional | Enabled 3-letter currency from GET /v1/currencies. Defaults to USD. |
| asset | string | required | Symbol such as ETH, or shorthand such as USDT.TRC20. |
| network | string | conditional | Required when a symbol exists on multiple networks. Example: ERC20. |
| order_ref | string | optional | Your order identifier, maximum 128 characters. Returned in API responses and events. |
| customer_email | string | optional | Valid email address, maximum 190 characters. Stored with the merchant payment record. |
| redirect_url | string | optional | HTTPS URL, maximum 255 characters, offered after successful checkout. |
Response fields
| Field | Type | Description |
|---|---|---|
| id | string | Stable payment identifier beginning with P-. |
| status | string | Current lifecycle state. |
| amount / amount_decimal / amount_minor | number / string / integer | Requested presentment value in convenient, exact-decimal and ISO minor-unit forms. |
| currency / currency_minor_units | string / integer | Locked presentment currency and its precision. |
| amount_usd / amount_usd_cents | number / integer | Immutable internal USD accounting value. |
| fx_rate_usd / fx_source / fx_observed_at | decimal string / string / ISO 8601 | Locked USD-per-presentment-unit snapshot and its audit metadata. |
| asset / network | string | Resolved on-chain asset. |
| crypto_amount | decimal string | Exact amount the customer must send. Never parse crypto decimals as binary floats. |
| crypto_received | decimal string | Total currently observed at the deposit address. |
| deposit_address | string | Dedicated address allocated for this payment. |
| exchange_rate | decimal string | Locked crypto/USD rate used to compute crypto_amount; this field keeps its original v1 meaning. |
| exchange_rate_source / exchange_rate_observed_at | string / ISO 8601 | Immutable crypto-rate audit snapshot. |
| confirmations | integer | Current network confirmations. |
| confirmations_required | integer | Threshold for this payment. Higher USD tiers may require additional confirmations. |
| checkout_url | URL | Hosted invoice to show the customer. |
| expires_at | ISO 8601 | Deadline for an unpaid invoice. |
| completed_at | ISO 8601 / null | Final settlement time when complete. |
Both live conversions are validated for freshness, source count and divergence before creation. If verification fails, creation returns an error instead of using a stale rate. The crypto amount is rounded upward at a useful asset precision, so rounding never leaves the merchant short.
List payments
/v1/payments240 requests / minute / keyReturns newest payments first for the authenticated merchant account. Use cursor pagination for reconciliation and exact filters to locate an order without walking the full history.
Query parameters
| Parameter | Default | Description |
|---|---|---|
| limit | 20 | Page size from 1 to 100. |
| starting_after | — | Payment ID returned as the previous page's next_cursor. |
| status | — | Exact lifecycle status such as pending, completed or expired. |
| order_ref | — | Exact merchant order reference, maximum 128 characters. |
curl "https://cryptopayin.com/v1/payments?status=completed&limit=20" \
--header "Authorization: Bearer $CPI_SECRET_KEY"{
"object": "list",
"data": [{
"id": "P-9F27C1E4KD",
"object": "payment",
"status": "completed",
"amount": 49.99,
"currency": "USD",
"asset": "ETH",
"network": "mainnet",
"crypto_amount": "0.01388612",
"crypto_received": "0.01388612",
"order_ref": "order_1042",
"confirmations": 12,
"confirmations_required": 12,
"completed_at": "2026-07-17T13:12:42+00:00"
}],
"has_more": true,
"next_cursor": "P-9F27C1E4KD"
}When has_more is true, pass next_cursor unchanged as starting_after. Unknown or repeated array-style query parameters are rejected instead of ignored.
Retrieve a payment
/v1/payments/{id}240 requests / minute / keyReturns the same payment object as creation with fresh status, received amount, transaction hash and confirmations. A key can only retrieve payments belonging to its merchant account.
curl https://cryptopayin.com/v1/payments/P-9F27C1E4KD \
--header "Authorization: Bearer $CPI_SECRET_KEY"Webhooks should drive normal order updates. Use retrieval to reconcile after a timeout, verify an event, render a backend status page or repair missed deliveries.
Cancel a payment
/v1/payments/{id}/cancel60 requests / minute / keyCloses an invoice the customer is not going to pay, so an abandoned checkout stops occupying your dashboard and its deposit address is released from monitoring. Requires the payments:write scope, and a key can only cancel payments belonging to its own merchant account.
curl --request POST https://cryptopayin.com/v1/payments/P-9F27C1E4KD/cancel \
--header "Authorization: Bearer $CPI_SECRET_KEY"Returns the full payment object with status set to cancelled. Calling it again on an already cancelled payment returns 200 with the same object, so a retry after a network timeout is safe.
| Response | When |
|---|---|
200 | Cancelled, or already cancelled. |
409 has_funds | The payment has received funds. It cannot be cancelled. |
409 not_cancellable | The payment is confirming, underpaid or already finished. |
404 not_found | No payment with that id on this account. |
Only an unfunded created or pending payment can be cancelled, and the check happens at the moment of the write — a payment funded between your read and this call keeps its status and the request fails. A cancelled invoice stops being watched, so cancelling one that already holds value would strand those funds: the gateway refuses rather than let that happen.
Payment lifecycle
Always treat the API status as authoritative. Do not infer completion from a browser redirect or from the customer saying they paid.
| Status | Meaning | Merchant action |
|---|---|---|
| created | Invoice and address allocated; no funding detected yet. | Show hosted checkout. |
| pending | Waiting for a usable on-chain payment. | Keep the order open. |
| underpaid | Funds arrived below the merchant's tolerance. | Ask the payer to send the displayed remainder. |
| confirming | Sufficient value detected; waiting for confirmations. | Do not fulfil yet. |
| completed | Required value and confirmations reached. | Fulfil exactly once. |
| overpaid | More than expected was confirmed. | Fulfil and review the excess. |
| expired | No qualifying payment was detected before expiry. | Create a fresh payment. |
| failed | Address allocation or processing failed. | Log the error and create a new payment. |
| cancelled | Closed on request through the cancel endpoint before any funds arrived. | Create a fresh payment if the customer returns. |
On-chain transfers are irreversible and CryptoPayIn has no refund mechanism — a confirmed payment is final. Any goodwill return is handled directly between you and your customer, outside the platform.
Hosted checkout
Each API payment includes a responsive checkout_url. It displays the merchant, requested presentment amount, locked USD equivalent when relevant, exact crypto amount, deposit address, QR code, network warning, countdown and live confirmation progress.
The customer sees the same crypto_amount returned by the API for the invoice window.
The payer does not create a CryptoPayIn account or share credentials.
The page polls the payment safely and moves from waiting to confirming to paid.
An HTTPS redirect_url is offered after success; it is not proof of payment.
Keep fulfilment on your backend. Browser navigation can be abandoned, repeated or forged; only a verified webhook or an authenticated GET proves the payment state.
Permissions & scopes
Each API key carries a fixed set of permissions chosen when you create it in Dashboard → Developers. Every endpoint checks the key's scopes before doing any work; a call outside a key's grant returns 403 insufficient_scope with an X-Required-Scope header naming the missing permission. Scopes are set once at creation and cannot be widened later — issue a new key instead. Keys created before scopes existed keep exactly their original ability: payments:read and payments:write.
| Scope | Grants | Endpoints |
|---|---|---|
payments:read | List and retrieve payments | GET /v1/payments, GET /v1/payments/{id} |
payments:write | Create and cancel hosted payments | POST /v1/payments, POST /v1/payments/{id}/cancel |
links:read | List and retrieve payment links | GET /v1/links, GET /v1/links/{id} |
links:write | Create, edit, pause and delete payment links | POST/PATCH/DELETE /v1/links |
shops:read | List and retrieve shops and their products | GET /v1/shops, GET .../products |
shops:write | Create and edit shops, products and variants | POST/PATCH/DELETE /v1/shops and products |
balance:read | Read crypto balances and USD estimates | GET /v1/balance |
payouts:read | List and retrieve payouts | GET /v1/payouts, GET /v1/payouts/{id} |
payouts:write | Request on-chain withdrawals | POST /v1/payouts |
payouts:write moves funds on-chain and is irreversible. Grant it only to keys you fully trust, keep those keys server-side, and prefer a dedicated key per automated process. GET /v1/account reports the calling key's scopes and your account limits.
Payment links
Reusable hosted links a customer can pay any number of times. A link owns the same pricing, asset, delivery and checkout-question logic as the dashboard link builder — the API simply drives it. An account holds up to 50 payment links.
/v1/linkslinks:read/v1/linkslinks:write/v1/links/{id}links:read/v1/links/{id}links:write/v1/links/{id}links:writeRequest body
| Field | Type | Requirement | Description |
|---|---|---|---|
| title | string | required | 3–120 characters. |
| description | string | optional | Up to 2,000 characters, shown on checkout. |
| template | string | optional | Checkout theme: signature (default), midnight, atelier, horizon, compact or ledger. |
| public_label | string | optional | Public seller name shown to buyers (2–80 characters). Never an account ID. |
| amount_type | string | optional | fixed (default) or open (customer chooses within min/max). |
| currency | string | optional | Presentment currency from GET /v1/currencies. Defaults to your account currency. |
| amount | number or string | conditional | Required for fixed. In currency at its ISO precision. |
| min / max | number or string | conditional | Bounds for open links. max may be 0/omitted for no ceiling. |
| accepted_assets | array of strings | optional | Asset codes such as ["BTC","USDT.TRC20"]. Omit for every available asset. |
| max_uses | integer | optional | Completed-payment cap. 0 means unlimited. |
| expires_at | ISO 8601 | optional | At least 5 minutes ahead, at most 12 months. UTC. |
| delivery_type | string | optional | none, text, url or keys — digital goods delivered after payment. |
| delivery_text / delivery_url | string | conditional | Content (≤50,000 chars) or an https URL for the matching delivery type. |
| delivery_keys | array of strings | conditional | One key per element for keys delivery. Up to 10,000, each ≤500 chars. |
| checkout_fields | array | optional | Up to 5 objects {label, type, required}; type is text, email, textarea or number. |
| success_message / redirect_url | string | optional | Post-payment message (≤500 chars) and an https redirect. |
| status | string | optional | PATCH only: active or paused. |
curl -X POST https://cryptopayin.com/v1/links \
-H "Authorization: Bearer $CPI_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Pro license",
"amount": 49.99,
"currency": "EUR",
"accepted_assets": ["BTC", "ETH", "USDT.TRC20"],
"delivery_type": "keys",
"delivery_keys": ["ABC-1", "ABC-2", "ABC-3"]
}'{
"id": "PL-N5PKTYB7",
"object": "payment_link",
"url": "https://cryptopaylink.co/pay/PL-N5PKTYB7",
"status": "active",
"title": "Pro license",
"amount_type": "fixed",
"currency": "EUR",
"amount": 49.99,
"amount_decimal": "49.99",
"accepted_assets": [{"asset":"BTC","network":"mainnet"},{"asset":"ETH","network":"mainnet"},{"asset":"USDT","network":"TRC20"}],
"uses": {"started": 0, "completed": 0, "in_flight": 0},
"delivery": {"type": "keys", "keys_available": 3, "keys_total": 3},
"expires_at": null,
"created_at": "2026-07-19T19:00:00+00:00"
}PATCH is a partial update: send only the fields you change and the rest are preserved, including unsold license keys. For keys links, sending delivery_keys replaces the pool of unsold keys; already-delivered keys are never touched. Deleting a link that has payments is refused implicitly by keeping its history intact.
Agent checkout
Every active payment link is also a machine-readable checkout: an AI agent or any script can discover it, create an invoice and read the delivery without a browser — and without any API key, because these are public buyer endpoints on the link domain, not merchant endpoints. Full contract and worked example: cryptopayin.com/agents.
https://cryptopaylink.co/pay/{link}.jsonpublic · discoveryReturns state, pricing, accepted assets and the exact input contract for the invoice call (required fields, shipping schema, variants).
https://cryptopaylink.co/pay/{link}/invoicepublic · supports Idempotency-KeyCreates the invoice through the same core, pricing snapshot and anti-abuse limits as the hosted page, and returns the deposit address, the exact crypto amount, a wallet URI and the receipt URL. The agent then pays on-chain from any wallet it controls.
https://cryptopaylink.co/pay/{link}/receipt?p={payment}public · poll every 5–10 sLive status and confirmations; once the payment completes, the response carries the delivery — your text content, private URL or a license key reserved for that payment — plus your success message and redirect URL.
Shops speak the same protocol
Storefronts expose the identical flow on their own domain: the catalogue with live stock, then a single call that validates the cart, reserves stock and returns the invoice.
https://shopycrypto.com/s/{shop}.jsonpublic · cataloguehttps://shopycrypto.com/s/{shop}/orderpublic · cart → invoice, supports Idempotency-Keyhttps://shopycrypto.com/s/{shop}/o/{order}/receipt?p={payment}public · poll every 5–10 sSeller controls
Agent checkout is on by default and costs the same flat 1%. Turn it off account-wide in Dashboard → Settings → General → AI & agent checkout: machine endpoints on links and shops then answer 403 agents_disabled while your human checkout pages keep working. Payments created by agents carry no special flag — they are ordinary payments in your dashboard, webhooks and exports.
Shops
A hosted storefront that groups products under one branded page. An account holds up to 10 shops. Products are managed through the nested product endpoints below.
/v1/shopsshops:read/v1/shopsshops:write/v1/shops/{id}shops:read/v1/shops/{id}shops:write/v1/shops/{id}shops:writeRequest body
| Field | Type | Requirement | Description |
|---|---|---|---|
| name | string | required | 2–80 characters. |
| tagline | string | optional | Up to 160 characters. |
| theme | string | optional | light (default) or dark. |
| accent | string | optional | Hex accent from the shop palette returned as accent_palette on GET /v1/shops. |
| accepted_assets | array of strings | optional | Default assets for the shop's products, e.g. ["BTC","LTC","XMR"]. Applied to all products when changed. |
| status | string | optional | PATCH only: active or paused. |
A shop disabled by CryptoPayIn for policy reasons cannot be re-activated or deleted through the API and returns admin_disabled (403). Deleting a shop removes its products; past payments remain untouched.
Products & variants
Products live inside a shop. Each shop holds up to 50 products. A product can be digital (with instant delivery) or physical (with shipping countries), and can expose up to 30 variant combinations built from 1–3 option groups.
/v1/shops/{shop}/productsshops:read/v1/shops/{shop}/productsshops:write/v1/shops/{shop}/products/{id}shops:read/v1/shops/{shop}/products/{id}shops:write/v1/shops/{shop}/products/{id}shops:writeRequest body
| Field | Type | Requirement | Description |
|---|---|---|---|
| title | string | required | 3–120 characters. |
| description / blurb | string | optional | Full description and a ≤200-char shop-card line. |
| emoji | string | optional | Single emoji shown on the product card. |
| featured | boolean | optional | At most one featured product per shop. |
| product_type | string | optional | digital (default) or physical. |
| shipping_countries | array of strings | conditional | Physical only: ISO codes such as ["FR","BE"], or ["*"] for worldwide. |
| amount_type / currency / amount / min / max | mixed | conditional | Base pricing, identical rules to payment links. Physical products must be fixed. |
| max_uses | integer | optional | Total sales cap (0 = unlimited). |
| delivery_type + delivery_text/url/keys | mixed | optional | Digital delivery for the base product, same shapes as payment links. |
| variant_options | array | optional | 1–3 groups {name, values[]}, each 2–10 values. Combinations must not exceed 30. |
| variants | array | conditional | One object per combination (see below). Required and exhaustive when variant_options is present. |
| status | string | optional | PATCH only: active or paused. |
Variant object
| Field | Type | Description |
|---|---|---|
| options | array of strings | One value per option group, in group order, e.g. ["Pro","Lifetime"]. |
| price | number or string | Variant price in the product currency. |
| stock | integer or null | Remaining units, or null for unlimited. |
| delivery_type + delivery_text/url/keys | mixed | Optional per-variant digital delivery override (inherit by default). Keys must be unique across the whole product. |
curl -X POST https://cryptopayin.com/v1/shops/SH-JYRK8TFJ/products \
-H "Authorization: Bearer $CPI_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Software license",
"amount": 30,
"currency": "USD",
"variant_options": [
{"name": "Edition", "values": ["Standard", "Pro"]},
{"name": "Term", "values": ["1 year", "Lifetime"]}
],
"variants": [
{"options": ["Standard","1 year"], "price": 30, "delivery_type": "keys", "delivery_keys": ["S1Y-1"]},
{"options": ["Standard","Lifetime"], "price": 79, "stock": 10, "delivery_type": "keys", "delivery_keys": ["SLT-1"]},
{"options": ["Pro","1 year"], "price": 59, "delivery_type": "keys", "delivery_keys": ["P1Y-1"]},
{"options": ["Pro","Lifetime"], "price": 149, "stock": 5, "delivery_type": "keys", "delivery_keys": ["PLT-1"]}
]
}'PATCH preserves existing orders and delivered keys. To adjust prices or stock, resend the matching variants array; combinations you omit are paused if they have orders, otherwise removed. A product can hold at most 10,000 active license keys across its base and variants, and every key must be unique within the product.
Balance
/v1/balancebalance:readReturns your settled crypto balances per asset with a best-effort USD estimate and the network fee charged on a withdrawal. Internal accounting is always USD; balances accrue from completed payments net of the merchant fee.
{
"object": "list",
"accounting_currency": "USD",
"minimum_payout_usd": 25,
"data": [{
"asset": "USDT",
"network": "TRC20",
"amount": "99.099",
"usd_estimate": 99.02,
"payout_network_fee": "2.445463"
}]
}usd_estimate is null when a verified live rate is momentarily unavailable; the underlying balance is still exact. Use these values to decide withdrawals, not for final accounting.
Withdrawals
Move settled crypto to an external wallet. Withdrawals are irreversible, so this endpoint enforces every safeguard the dashboard does: a valid destination for the asset, a live verified rate, the account minimum, sufficient balance including the network fee, and two-factor confirmation when your account has 2FA enabled.
/v1/payoutspayouts:read/v1/payoutspayouts:write · 30 / min / key/v1/payouts/{id}payouts:readRequest body
| Field | Type | Requirement | Description |
|---|---|---|---|
| asset | string | required | Symbol or shorthand, e.g. LTC or USDT.TRC20. |
| network | string | conditional | Required when the symbol exists on multiple networks. |
| amount | number or string | required | Amount to send, excluding the network fee, at the asset's precision. Its USD value must meet the account minimum. |
| address | string | required | Destination address, validated for the asset's chain. |
| note | string | optional | Your own reference, up to 255 characters. |
| totp_code | string | conditional | Current 6-digit or recovery code. Required when 2FA is enabled on the account. |
curl -X POST https://cryptopayin.com/v1/payouts \
-H "Authorization: Bearer $CPI_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: withdraw-2026-07-19-01" \
-d '{
"asset": "USDT.TRC20",
"amount": "50",
"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"note": "weekly settlement"
}'{
"id": "W-EM64ZCJB",
"object": "payout",
"status": "requested",
"asset": "USDT",
"network": "TRC20",
"amount": "50",
"fee": "2.445463",
"total_debited": "52.445463",
"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"txid": null,
"confirmations": 0,
"created_at": "2026-07-19T19:10:00+00:00"
}Status lifecycle
| Status | Meaning |
|---|---|
requested | Reserved from your balance; awaiting operator review or auto-approval. |
approved / processing | Cleared and queued for broadcast by the executor. |
sent | Broadcast on-chain; txid is populated. |
confirmed | Reached the required confirmations. Final. |
failed | Could not be sent; failure_message explains why and the balance is returned. |
cancelled | Cancelled before broadcast; the reserved balance is returned. |
Send an Idempotency-Key so a network retry can never create a second withdrawal: the same key with the same body returns the original payout (Idempotent-Replayed: true); the same key with a different body returns 409 idempotency_conflict. The 2FA code is deliberately excluded from the idempotency fingerprint so a rotating code does not trigger a false conflict. The reservation debits your balance immediately; a failed or cancelled payout returns it.
Account
/v1/accountany valid keyReturns your account profile, the calling key's scopes, your platform fee and every live limit — useful for a self-configuring integration or a pre-flight check.
{
"object": "account",
"id": "MC67T3PHQZ",
"fee_bps": 100,
"fee_percent": 1,
"default_currency": "USD",
"two_factor_enabled": false,
"api_key": {"label": "production", "scopes": ["payments:read","payments:write"]},
"limits": {
"shops": {"used": 1, "max": 10},
"payment_links": {"used": 0, "max": 50},
"products_per_shop_max": 50,
"variant_combinations_per_product_max": 30,
"license_keys_per_product_max": 10000,
"active_api_keys_max": 50,
"checkout_fields_per_link_max": 5
},
"minimum_payout_usd": 25
}Webhooks
Add up to 10 public HTTPS endpoints in Dashboard -> Developers. Each endpoint receives its own whsec_... signing secret, shown once. It can listen account-wide or be tied to a specific active API key; key-scoped endpoints receive only payments created with that key.
Verify before parsing
CryptoPayIn signs the exact raw request body using the endpoint secret. Version 1 signs timestamp + "." + raw_body. Reject stale timestamps before accepting the event.
import crypto from "node:crypto";
const timestamp = req.headers["x-cpi-timestamp"];
const signature = req.headers["x-cpi-signature"];
const rawBody = req.rawBody; // Buffer captured before JSON parsing
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
throw new Error("stale webhook");
}
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.CPI_WEBHOOK_SECRET)
.update(Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]))
.digest("hex");
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!valid) throw new Error("invalid webhook signature");
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_CPI_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_X_CPI_SIGNATURE'] ?? '';
if (!ctype_digit($timestamp) || abs(time() - (int)$timestamp) > 300) {
http_response_code(400); exit('stale webhook');
}
$expected = 'sha256=' . hash_hmac(
'sha256', $timestamp . '.' . $rawBody, getenv('CPI_WEBHOOK_SECRET')
);
if (!hash_equals($expected, $received)) {
http_response_code(401); exit('invalid signature');
}
$event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
import hashlib, hmac, os, time
raw_body = request.get_data() # bytes, before JSON decoding
timestamp = request.headers.get("X-CPI-Timestamp", "")
received = request.headers.get("X-CPI-Signature", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
raise ValueError("stale webhook")
signed = timestamp.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(
os.environ["CPI_WEBHOOK_SECRET"].encode(), signed, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, received):
raise ValueError("invalid signature")Delivery headers
| Header | Example | Purpose |
|---|---|---|
| Content-Type | application/json | UTF-8 JSON body. |
| X-CPI-Timestamp | 1784293200 | Unix seconds included in the signed message. |
| X-CPI-Signature | sha256=... | Hex HMAC-SHA256. |
| X-CPI-Signature-Version | v1 | Signing scheme version. |
| X-CPI-Event-Id | evt_a12b... | Stable logical event ID; identical across retries. |
| X-CPI-Delivery-Id | 1842 | Stable endpoint delivery record ID. |
Payment payload
{
"event_id": "evt_a12b34c56d78e90f12345678",
"event": "payment.completed",
"id": "P-9F27C1E4KD",
"status": "completed",
"order_ref": "order_1042",
"amount": 49.99,
"amount_decimal": "49.99",
"amount_minor": 4999,
"currency_minor_units": 2,
"currency": "USD",
"amount_usd": 49.99,
"amount_usd_cents": 4999,
"fx_rate_usd": "1.000000000000",
"fx_source": "fixed:USD",
"fx_observed_at": "2026-07-17T13:00:00Z",
"fx_discrepancy_bps": 0,
"exchange_rate": "3600.00000000",
"exchange_rate_currency": "USD",
"exchange_rate_source": "median:cb,cg,cl",
"exchange_rate_source_count": 3,
"exchange_rate_observed_at": "2026-07-17T13:00:00Z",
"exchange_rate_discrepancy_bps": 12,
"asset": "ETH",
"network": "mainnet",
"crypto_amount": "0.01388612",
"crypto_received": "0.01388612",
"txid": "0x9d81...75af",
"confirmations": 12,
"confirmations_required": 12,
"deposit_address": "0x71b8c3d4700000000000000000000000000084e2",
"sent_at": "2026-07-17T13:12:42Z"
}Retries and endpoint safety
A delivery succeeds on HTTP 200-299. Do expensive work asynchronously and answer quickly.
The exact body and event ID are retained; failures retry after roughly 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours.
3xx responses are not followed. Register the final HTTPS URL directly.
Private, loopback, link-local and reserved IPs are blocked; every DNS answer is validated and the connection is pinned.
Deliveries are at least once. Make your handler idempotent by recording event_id with a unique constraint before fulfilment. Retrieve the API object when reconciling an unexpected event.
Event reference
payment.completedExpected value confirmed.payment.overpaidMore than expected confirmed.payment.underpaidFunding below tolerance detected.payment.expiredUnpaid invoice window closed.payment.failedPayment setup or processing failed.payout.sentWithdrawal broadcast on-chain.payout.confirmedWithdrawal reached confirmations.payout.failedWithdrawal could not complete.webhook.testManual connectivity test.Payout event shape
{
"event_id": "evt_b98c76d54e32a10f87654321",
"event": "payout.sent",
"id": "W-8J2K7M4RQP",
"status": "sent",
"asset": "ETH",
"network": "mainnet",
"amount": "0.25",
"fee": "0.00081768",
"address": "0x84f2...9bc1",
"txid": "0xa1c4...07ee",
"confirmations": 0,
"note": "weekly treasury",
"sent_at": "2026-07-17T14:08:31Z"
}Errors and rate limits
Errors always use one JSON envelope. Branch on error.type; the human message may be improved without a version change. Quote error.request_id or the matching X-Request-Id response header when contacting support.
{
"error": {
"type": "ambiguous_asset",
"message": "specify network (e.g. USDT.ERC20 or USDT.TRC20)",
"request_id": "b942e21f8dca4b06b8672eb9"
}
}| HTTP | Typical types | Meaning |
|---|---|---|
| 400 | invalid_request, unknown_parameter | Malformed JSON, query or idempotency header. |
| 401 | unauthorized | Missing, invalid or inactive credential/account. |
| 403 | insufficient_scope, admin_disabled | Valid key lacking the required permission (see X-Required-Scope), or a resource an administrator locked. |
| 404 | not_found | Unknown endpoint, or a resource outside this merchant account. |
| 405 | method_not_allowed | Use the method shown in the Allow header. |
| 409 | idempotency_conflict | Key reused with different JSON. |
| 413 | request_too_large | JSON body exceeds 64 KiB. |
| 415 | unsupported_media_type | POST body is not declared as application/json. |
| 422 | invalid, limit, invalid_amount, below_minimum, bad_address, 2fa_required, unsupported_asset | Well-formed request failed deterministic validation or hit a resource cap. |
| 429 | rate_limited | Wait for Retry-After. |
| 500 | server_error | Unexpected failure; retry safely with the same idempotency key. |
| 503 | maintenance, temporarily_unavailable, asset_unavailable, no_rate, price_stale, fiat_rate_unavailable, fiat_rate_stale, derive_failed | Transient platform, node, price or address-allocation failure. No stale conversion is substituted. |
Current limits
| Scope | Limit | Window |
|---|---|---|
| Nginx per-IP safety ceiling | 10 requests/second, burst 30 | Continuous |
| Unauthenticated IP ceiling | 300 requests | 60 seconds |
| POST /v1/payments, links, shops, products | 120 requests per API key | 60 seconds |
| POST /v1/payments/{id}/cancel | 60 requests per API key | 60 seconds |
| POST /v1/payouts | 30 requests per API key | 60 seconds |
| GET endpoints | 240 requests per API key | 60 seconds |
Account limits
| Resource | Cap |
|---|---|
| Shops per account | 10 |
| Payment links per account | 50 |
| Products per shop | 50 |
| Variant combinations per product | 30 |
| License keys per product / link | 10,000 |
| Checkout questions per link | 5 |
| Active API keys per account | 50 |
Read your live usage against these caps from GET /v1/account.
Successful application-limited responses expose X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Retry 429, 500 and 503 with exponential backoff and jitter. For POST, always reuse the original Idempotency-Key and identical JSON.
Integration security
Load them at runtime; never log the full value or commit it to source control.
A browser or mobile client cannot safely hold a merchant secret.
Check timestamp freshness and use constant-time signature comparison before JSON parsing.
Record processed events/orders transactionally so retries never ship twice.
Retrieve the payment when an event is unexpected or your local state disagrees.
Create a replacement key, deploy it, verify traffic, then delete the old key.
Account access is controlled by a non-recoverable 16-digit merchant key, optionally protected by TOTP. Store both merchant access and API secrets with the same care as wallet credentials.
Go-live checklist
Do not reuse a developer's personal copy across services.
Use GET /v1/assets and GET /v1/currencies; render only entries that are present and available: true.
Save the signing secret once; verify the timestamp and signature, then deduplicate the stable event ID.
Exercise a duplicate retry and confirm only one payment ID exists.
Your order state should remain safe through stale fiat rates, unavailable assets, delayed confirmations and every non-happy path.
Compare your orders with API payment states, webhook logs and the merchant ledger.
Ready to integrate?
Create an account in seconds, generate a key and keep this reference beside your code.