CryptoPayIn
Developer documentation

Build payments that settle on-chain.

Everything needed to create a payment, send a customer to hosted checkout, follow confirmations and process signed webhooks in production.

API base URLVersion 1
https://cryptopayin.com/v1
ProtocolREST / JSON
AuthenticationBearer secret
ModeLive only
Introduction

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.

Base URL/v1
Fiat amountsISO minor units
Crypto valuesDecimal strings
Default expiry30 minutes
!

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

1Create a key

Generate a secret once in Dashboard -> Developers.

2Create payment

POST the order currency, amount and selected asset.

3Open checkout

Send the customer to the returned hosted URL.

4Process event

Verify the HMAC and update your order idempotently.

Get started

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"
  }'

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
}
Credentials

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.

AUTHAuthorization: Bearer csk_live_...Every endpoint
Shown once

The 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.

Independent keys

Create separate keys per application or environment and revoke them independently. An account can hold up to 50 active keys.

Server-side only

Never put a csk_live_ value in browser JavaScript, a mobile binary, a public repository or a checkout page.

Webhook scoping

A webhook endpoint can be account-wide or attached to one API key, keeping integrations isolated.

i

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.

Safe retries

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.

CaseResultHTTP
First useCreates and returns a new payment.201
Same key + same JSONReturns the existing payment with Idempotent-Replayed: true.200
Same key + different JSONRejects 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.

REST API

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.

GET/v1/assetsDiscover available assets
GET/v1/currenciesDiscover fiat presentment currencies
POST/v1/paymentsCreate a payment
GET/v1/paymentsList and filter payments
GET/v1/payments/{id}Retrieve a payment
POST/v1/payments/{id}/cancelCancel an unfunded payment
i

Version 1 may receive backward-compatible fields and endpoints. A breaking contract change will use a new base path rather than silently changing /v1.

API reference

List assets

GET/v1/assetsBearer auth required

Use 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

AssetNetwork valueShorthandBase confirmations
BTCmainnetBTC2
ETHmainnetETH6
USDTTRC20 / ERC20USDT.TRC2019 / 6
USDCERC20USDC.ERC206
DAIERC20DAI6
SHIBERC20SHIB6
PEPEERC20PEPE6
LTCmainnetLTC6
TRXmainnetTRX19
DOGEmainnetDOGE20
XMRmainnetXMR10
SOLmainnetSOL32
!

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.

API reference

List fiat currencies

GET/v1/currenciesBearer auth required

Returns 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"
  }]
}
i

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.

API reference

Create a payment

POST/v1/payments120 requests / minute / key

Creates 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

FieldTypeRequirementDescription
amountnumber or decimal stringrequiredValue in currency, at that currency's ISO precision. Its locked USD equivalent must not exceed $1,000,000.00; asset minimums also apply.
currencystringoptionalEnabled 3-letter currency from GET /v1/currencies. Defaults to USD.
assetstringrequiredSymbol such as ETH, or shorthand such as USDT.TRC20.
networkstringconditionalRequired when a symbol exists on multiple networks. Example: ERC20.
order_refstringoptionalYour order identifier, maximum 128 characters. Returned in API responses and events.
customer_emailstringoptionalValid email address, maximum 190 characters. Stored with the merchant payment record.
redirect_urlstringoptionalHTTPS URL, maximum 255 characters, offered after successful checkout.

Response fields

FieldTypeDescription
idstringStable payment identifier beginning with P-.
statusstringCurrent lifecycle state.
amount / amount_decimal / amount_minornumber / string / integerRequested presentment value in convenient, exact-decimal and ISO minor-unit forms.
currency / currency_minor_unitsstring / integerLocked presentment currency and its precision.
amount_usd / amount_usd_centsnumber / integerImmutable internal USD accounting value.
fx_rate_usd / fx_source / fx_observed_atdecimal string / string / ISO 8601Locked USD-per-presentment-unit snapshot and its audit metadata.
asset / networkstringResolved on-chain asset.
crypto_amountdecimal stringExact amount the customer must send. Never parse crypto decimals as binary floats.
crypto_receiveddecimal stringTotal currently observed at the deposit address.
deposit_addressstringDedicated address allocated for this payment.
exchange_ratedecimal stringLocked crypto/USD rate used to compute crypto_amount; this field keeps its original v1 meaning.
exchange_rate_source / exchange_rate_observed_atstring / ISO 8601Immutable crypto-rate audit snapshot.
confirmationsintegerCurrent network confirmations.
confirmations_requiredintegerThreshold for this payment. Higher USD tiers may require additional confirmations.
checkout_urlURLHosted invoice to show the customer.
expires_atISO 8601Deadline for an unpaid invoice.
completed_atISO 8601 / nullFinal 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.

API reference

List payments

GET/v1/payments240 requests / minute / key

Returns 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

ParameterDefaultDescription
limit20Page size from 1 to 100.
starting_afterPayment ID returned as the previous page's next_cursor.
statusExact lifecycle status such as pending, completed or expired.
order_refExact 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"
}
i

When has_more is true, pass next_cursor unchanged as starting_after. Unknown or repeated array-style query parameters are rejected instead of ignored.

API reference

Retrieve a payment

GET/v1/payments/{id}240 requests / minute / key

Returns 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"
i

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.

API reference

Cancel a payment

POST/v1/payments/{id}/cancel60 requests / minute / key

Closes 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.

ResponseWhen
200Cancelled, or already cancelled.
409 has_fundsThe payment has received funds. It cannot be cancelled.
409 not_cancellableThe payment is confirming, underpaid or already finished.
404 not_foundNo 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.

State model

Payment lifecycle

Always treat the API status as authoritative. Do not infer completion from a browser redirect or from the customer saying they paid.

created->pending->underpaidorconfirming->completed/overpaid
StatusMeaningMerchant action
createdInvoice and address allocated; no funding detected yet.Show hosted checkout.
pendingWaiting for a usable on-chain payment.Keep the order open.
underpaidFunds arrived below the merchant's tolerance.Ask the payer to send the displayed remainder.
confirmingSufficient value detected; waiting for confirmations.Do not fulfil yet.
completedRequired value and confirmations reached.Fulfil exactly once.
overpaidMore than expected was confirmed.Fulfil and review the excess.
expiredNo qualifying payment was detected before expiry.Create a fresh payment.
failedAddress allocation or processing failed.Log the error and create a new payment.
cancelledClosed 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.

Customer experience

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.

Rate locked

The customer sees the same crypto_amount returned by the API for the invoice window.

No customer account

The payer does not create a CryptoPayIn account or share credentials.

Live status

The page polls the payment safely and moves from waiting to confirming to paid.

Merchant redirect

An HTTPS redirect_url is offered after success; it is not proof of payment.

i

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.

Credentials

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.

ScopeGrantsEndpoints
payments:readList and retrieve paymentsGET /v1/payments, GET /v1/payments/{id}
payments:writeCreate and cancel hosted paymentsPOST /v1/payments, POST /v1/payments/{id}/cancel
links:readList and retrieve payment linksGET /v1/links, GET /v1/links/{id}
links:writeCreate, edit, pause and delete payment linksPOST/PATCH/DELETE /v1/links
shops:readList and retrieve shops and their productsGET /v1/shops, GET .../products
shops:writeCreate and edit shops, products and variantsPOST/PATCH/DELETE /v1/shops and products
balance:readRead crypto balances and USD estimatesGET /v1/balance
payouts:readList and retrieve payoutsGET /v1/payouts, GET /v1/payouts/{id}
payouts:writeRequest on-chain withdrawalsPOST /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.

Machine buyers

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.

GEThttps://cryptopaylink.co/pay/{link}.jsonpublic · discovery

Returns state, pricing, accepted assets and the exact input contract for the invoice call (required fields, shipping schema, variants).

POSThttps://cryptopaylink.co/pay/{link}/invoicepublic · supports Idempotency-Key

Creates 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.

GEThttps://cryptopaylink.co/pay/{link}/receipt?p={payment}public · poll every 5–10 s

Live 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.

GEThttps://shopycrypto.com/s/{shop}.jsonpublic · catalogue
POSThttps://shopycrypto.com/s/{shop}/orderpublic · cart → invoice, supports Idempotency-Key
GEThttps://shopycrypto.com/s/{shop}/o/{order}/receipt?p={payment}public · poll every 5–10 s

Seller 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.

Merchant resources

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.

GET/v1/shopsshops:read
POST/v1/shopsshops:write
GET/v1/shops/{id}shops:read
PATCH/v1/shops/{id}shops:write
DELETE/v1/shops/{id}shops:write

Request body

FieldTypeRequirementDescription
namestringrequired2–80 characters.
taglinestringoptionalUp to 160 characters.
themestringoptionallight (default) or dark.
accentstringoptionalHex accent from the shop palette returned as accent_palette on GET /v1/shops.
accepted_assetsarray of stringsoptionalDefault assets for the shop's products, e.g. ["BTC","LTC","XMR"]. Applied to all products when changed.
statusstringoptionalPATCH only: active or paused.
i

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.

Merchant resources

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.

GET/v1/shops/{shop}/productsshops:read
POST/v1/shops/{shop}/productsshops:write
GET/v1/shops/{shop}/products/{id}shops:read
PATCH/v1/shops/{shop}/products/{id}shops:write
DELETE/v1/shops/{shop}/products/{id}shops:write

Request body

FieldTypeRequirementDescription
titlestringrequired3–120 characters.
description / blurbstringoptionalFull description and a ≤200-char shop-card line.
emojistringoptionalSingle emoji shown on the product card.
featuredbooleanoptionalAt most one featured product per shop.
product_typestringoptionaldigital (default) or physical.
shipping_countriesarray of stringsconditionalPhysical only: ISO codes such as ["FR","BE"], or ["*"] for worldwide.
amount_type / currency / amount / min / maxmixedconditionalBase pricing, identical rules to payment links. Physical products must be fixed.
max_usesintegeroptionalTotal sales cap (0 = unlimited).
delivery_type + delivery_text/url/keysmixedoptionalDigital delivery for the base product, same shapes as payment links.
variant_optionsarrayoptional1–3 groups {name, values[]}, each 2–10 values. Combinations must not exceed 30.
variantsarrayconditionalOne object per combination (see below). Required and exhaustive when variant_options is present.
statusstringoptionalPATCH only: active or paused.

Variant object

FieldTypeDescription
optionsarray of stringsOne value per option group, in group order, e.g. ["Pro","Lifetime"].
pricenumber or stringVariant price in the product currency.
stockinteger or nullRemaining units, or null for unlimited.
delivery_type + delivery_text/url/keysmixedOptional 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"]}
    ]
  }'
i

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.

Merchant resources

Balance

GET/v1/balancebalance:read

Returns 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"
  }]
}
i

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.

Merchant resources

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.

GET/v1/payoutspayouts:read
POST/v1/payoutspayouts:write · 30 / min / key
GET/v1/payouts/{id}payouts:read

Request body

FieldTypeRequirementDescription
assetstringrequiredSymbol or shorthand, e.g. LTC or USDT.TRC20.
networkstringconditionalRequired when the symbol exists on multiple networks.
amountnumber or stringrequiredAmount to send, excluding the network fee, at the asset's precision. Its USD value must meet the account minimum.
addressstringrequiredDestination address, validated for the asset's chain.
notestringoptionalYour own reference, up to 255 characters.
totp_codestringconditionalCurrent 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

StatusMeaning
requestedReserved from your balance; awaiting operator review or auto-approval.
approved / processingCleared and queued for broadcast by the executor.
sentBroadcast on-chain; txid is populated.
confirmedReached the required confirmations. Final.
failedCould not be sent; failure_message explains why and the balance is returned.
cancelledCancelled 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.

Merchant resources

Account

GET/v1/accountany valid key

Returns 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
}
Server-to-server events

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");

Delivery headers

HeaderExamplePurpose
Content-Typeapplication/jsonUTF-8 JSON body.
X-CPI-Timestamp1784293200Unix seconds included in the signed message.
X-CPI-Signaturesha256=...Hex HMAC-SHA256.
X-CPI-Signature-Versionv1Signing scheme version.
X-CPI-Event-Idevt_a12b...Stable logical event ID; identical across retries.
X-CPI-Delivery-Id1842Stable 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

Return any 2xx

A delivery succeeds on HTTP 200-299. Do expensive work asynchronously and answer quickly.

Six total attempts

The exact body and event ID are retained; failures retry after roughly 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours.

No redirects

3xx responses are not followed. Register the final HTTPS URL directly.

Public destinations only

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.

Webhooks

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"
}
Reliability

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"
  }
}
HTTPTypical typesMeaning
400invalid_request, unknown_parameterMalformed JSON, query or idempotency header.
401unauthorizedMissing, invalid or inactive credential/account.
403insufficient_scope, admin_disabledValid key lacking the required permission (see X-Required-Scope), or a resource an administrator locked.
404not_foundUnknown endpoint, or a resource outside this merchant account.
405method_not_allowedUse the method shown in the Allow header.
409idempotency_conflictKey reused with different JSON.
413request_too_largeJSON body exceeds 64 KiB.
415unsupported_media_typePOST body is not declared as application/json.
422invalid, limit, invalid_amount, below_minimum, bad_address, 2fa_required, unsupported_assetWell-formed request failed deterministic validation or hit a resource cap.
429rate_limitedWait for Retry-After.
500server_errorUnexpected failure; retry safely with the same idempotency key.
503maintenance, temporarily_unavailable, asset_unavailable, no_rate, price_stale, fiat_rate_unavailable, fiat_rate_stale, derive_failedTransient platform, node, price or address-allocation failure. No stale conversion is substituted.

Current limits

ScopeLimitWindow
Nginx per-IP safety ceiling10 requests/second, burst 30Continuous
Unauthenticated IP ceiling300 requests60 seconds
POST /v1/payments, links, shops, products120 requests per API key60 seconds
POST /v1/payments/{id}/cancel60 requests per API key60 seconds
POST /v1/payouts30 requests per API key60 seconds
GET endpoints240 requests per API key60 seconds

Account limits

ResourceCap
Shops per account10
Payment links per account50
Products per shop50
Variant combinations per product30
License keys per product / link10,000
Checkout questions per link5
Active API keys per account50

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.

Production safety

Integration security

Keep API secrets in a secret manager

Load them at runtime; never log the full value or commit it to source control.

Call the API from your backend

A browser or mobile client cannot safely hold a merchant secret.

Verify the raw webhook body

Check timestamp freshness and use constant-time signature comparison before JSON parsing.

Make fulfilment idempotent

Record processed events/orders transactionally so retries never ship twice.

Trust final API state, not redirects

Retrieve the payment when an event is unexpected or your local state disagrees.

Rotate by overlap

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.

Launch

Go-live checklist

1
Create a dedicated production API key

Do not reuse a developer's personal copy across services.

2
Query both live catalogues

Use GET /v1/assets and GET /v1/currencies; render only entries that are present and available: true.

3
Add and test your webhook endpoint

Save the signing secret once; verify the timestamp and signature, then deduplicate the stable event ID.

4
Use an idempotency key for every order

Exercise a duplicate retry and confirm only one payment ID exists.

5
Test rate outages, underpayment and expiry

Your order state should remain safe through stale fiat rates, unavailable assets, delayed confirmations and every non-happy path.

6
Reconcile daily

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.

Create account