Merchant API v1
Build your
payment integration.
Create a payment from your backend, send your customer to checkout, and let signed webhooks keep your orders in sync.
- 01 / Your serverCreate an invoice
Lock a quote for the order.
- 02 / Your customerPay at checkout
Scan, send, and follow progress.
- 03 / Your backendConfirm the order
Verify the event before fulfillment.
Ask your Coinplexo operator for your API base URL, merchant API key, enabled assets, and a test environment. Have a public HTTPS webhook endpoint ready. Examples use placeholder domains and a sample order.
Every merchant API request needs a key. Keep it in your backend’s environment or secret manager.
Use your merchant API key
Send X-API-Key with each request to /api/v1. Authorization: Bearer <key> is also supported. Keys are scoped to one merchant and issued by your operator.
Replace https://gateway.example.com with your API origin, without a trailing slash. The examples below use these environment variables.
Run shell examples in Bash, Zsh, or WSL. Use a current Node.js release with built-in fetch for JavaScript examples.
# Use the API origin supplied by your Coinplexo operator.
export COINPLEXO_API_URL="https://gateway.example.com"
export COINPLEXO_API_KEY="replace-with-your-merchant-key"
curl --fail-with-body "$COINPLEXO_API_URL/api/v1/assets" \
-H "X-API-Key: $COINPLEXO_API_KEY"| Scope | Allows |
|---|---|
invoices:write | Create payments. |
invoices:read | Read invoices and payments. |
merchant:read | Read your ledger balances. |
webhooks:test | Send a signed test event. |
A payment starts with an invoice: an order amount, a selected crypto asset, and a time-limited exchange rate.
/api/v1/invoicesinvoices:write- Choose an available asset. Read
GET /api/v1/assets. An asset ID identifies its network too:USDC_SOL, for example, is USDC on Solana. Confirm it is allowed for your merchant. - Check the minimum. Read
GET /api/v1/min-amount?asset=USDC_SOL¤cy=USD. Minimums and fees are configurable; use the response rather than hard-coding them. - Create the invoice on your backend. Use the order amount from your database and a stable idempotency key for this payment attempt.
curl --fail-with-body -X POST \
"$COINPLEXO_API_URL/api/v1/invoices" \
-H "X-API-Key: $COINPLEXO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-10492-attempt-1" \
-d '{
"external_order_id": "order-10492",
"asset": "USDC_SOL",
"fiat_currency": "USD",
"fiat_amount": "49.00",
"description": "Pro plan — 1 month",
"success_url": "https://shop.example.com/thanks",
"cancel_url": "https://shop.example.com/cart",
"ttl_seconds": 3600
}'| Field | Requirement | What to send |
|---|---|---|
asset | Required | An enabled asset ID allowed for your merchant. |
fiat_amount | Required | A positive decimal string, such as "49.00". |
external_order_id | Recommended | Your order reference, up to 128 characters. Save the returned invoice ID too. |
fiat_currency | Optional | For example, USD. Defaults to your merchant’s currency. |
success_urlcancel_url | Optional | Your HTTPS return pages. Ask the operator to allow your callback domains. |
ttl_seconds | Optional | Quote lifetime: 300–86400 seconds. Default: 3600. |
description | Optional | Customer-facing order description, up to 500 characters. |
customer_refmetadata | Optional | A customer reference (128 characters max) and up to 20 metadata keys. Avoid sensitive data. |
A successful create request returns HTTP 201 and an invoice. Store its ID with your order, then redirect to checkout_url.
Let checkout handle the payment
The hosted page shows the crypto amount, deposit address, QR code, expiry timer, and live confirmation progress. No customer account or API key is needed.
The asset is selected when you create the invoice. Offer supported asset choices in your own checkout before calling the API.
All crypto amounts are integer strings in base units. For a 6-decimal asset, "49000000" represents 49 tokens. Use exact decimal or integer arithmetic.
The example response is shortened. Use expected_amount from the API: it includes any processing fee (see below).
{
"id": "0fd1a33e-894b-4c14-bb44-5c67a65e2450",
"external_order_id": "order-10492",
"status": "WAITING_PAYMENT",
"network_id": "solana",
"asset_id": "USDC_SOL",
"asset_decimals": 6,
"fiat_currency": "USD",
"fiat_amount": "49.00",
"processing_fee_fiat": "0",
"processing_fee_units": "0",
"expected_amount": "49000000",
"checkout_url": "https://pay.example.com/invoice/0fd1a33e-894b-4c14-bb44-5c67a65e2450"
}Processing fees
Some assets carry a fixed processing fee that the customer pays on top of your order amount. It covers the network cost of moving the payment to cold storage. Today: USDT on TRON: $1.00 per invoice, TRX: $0.10, all other assets: none. Fees can change; read processing_fee_fiat from GET /api/v1/min-amount before showing a total.
The fee is converted at the invoice's locked rate and included in expected_amount. The invoice also returns it as processing_fee_fiat and processing_fee_units. You are credited fiat_amount; the fee is never part of your balance. The hosted checkout shows price, fee, and total on separate lines.
# USDT on TRON, $10 order
GET /api/v1/min-amount?asset=USDT_TRON¤cy=USD
{ "items": [{ "asset_id": "USDT_TRON", "min_fiat": "9.99",
"processing_fee_fiat": "1", "enabled": true }] }
POST /api/v1/invoices { "asset": "USDT_TRON", "fiat_amount": "10.00", ... }
{
"fiat_amount": "10.00", // credited to you
"processing_fee_fiat": "1", // paid by the customer
"processing_fee_units": "1001192",
"expected_amount": "11013106", // 11.013106 USDT, what the customer sends
...
}Show the fee to the customer before they choose an asset. A customer paying USDT on TRON also pays TRON's own energy cost from their wallet; that is a network charge outside the invoice amount.
| Status | Merchant action |
|---|---|
CREATED · WAITING_PAYMENT | Show the checkout link and wait. |
PAYMENT_DETECTED · CONFIRMING | Payment is in progress. Wait for final confirmation. |
PARTIALLY_PAID | More funds are needed; do not fulfill yet. |
PAID · OVERPAID | Reconcile the invoice with the order and fulfill once. Handle any excess through your support process. |
EXPIRED | Offer a new invoice. Stop requesting funds to the expired quote. |
UNDERPAID · LATE_PAYMENT · MANUAL_REVIEW · FAILED | Hold fulfillment and ask your operator to review. |
Read the latest status
Set INVOICE_ID to the saved invoice ID. Use this endpoint to reconcile a webhook or recover when a notification is delayed. Compare the invoice’s order reference, currency, amount, and status to your own order.
curl --fail-with-body \
"$COINPLEXO_API_URL/api/v1/invoices/$INVOICE_ID" \
-H "X-API-Key: $COINPLEXO_API_KEY"Receive payment updates at your backend without keeping a browser open or continuously polling.
- Deploy a public HTTPS endpoint. For example,
https://shop.example.com/api/coinplexo/webhook. It must accept JSON POST requests without a browser login. - Ask your operator to register it. In the operator dashboard: Merchants → your merchant → Webhooks → Add endpoint. Registration and secret rotation are managed by the operator, not the merchant API.
- Select events and save the signing secret. Subscribe to
payment.confirmed, plus the lifecycle events your app handles. Includewebhook.testto receive test deliveries. Store the endpoint’s secret inCOINPLEXO_WEBHOOK_SECRET; it is separate from your API key and shown only on creation or rotation. - Send a test event. Use a key with
webhooks:test. HTTP 202 means queued, not delivered; check your receiver logs and the operator’s delivery history.
curl --fail-with-body -X POST \
"$COINPLEXO_API_URL/api/v1/webhooks/test" \
-H "X-API-Key: $COINPLEXO_API_KEY"
# 202 Accepted
# { "event_id": "…", "status": "queued" }Delivery headers
- Coinplexo-Event-Id
- Stable event ID for duplicate detection. Read it from the header, not the JSON body.
- Coinplexo-Event-Type
- The event name, such as
payment.confirmed. - Coinplexo-Delivery-Attempt
- The current attempt number.
- Coinplexo-Signature
t=<unix-seconds>,v1=<hex-signature>
{
"event_type": "payment.confirmed",
"invoice_id": "0fd1a33e-894b-4c14-bb44-5c67a65e2450",
"external_order_id": "order-10492",
"status": "PAID",
"network": "solana",
"asset": "USDC_SOL",
"fiat_amount": "49.00",
"fiat_currency": "USD",
"expected_amount": "49000000",
"confirmed_amount": "49000000"
}Accept once, process reliably
Verify the signature, durably store the event, then respond with any 2xx. Process it in a worker. Use a unique event-ID constraint for duplicate delivery and an atomic order or invoice check to prevent duplicate fulfillment. Reconcile against the latest invoice state; do not depend on event arrival order.
Delivery retries use exponential backoff starting at 30 seconds, capped at 6 hours, up to the configured retry limit. Non-retriable 4xx responses stop delivery; 408, 429, and 5xx can retry. If durable storage is unavailable, return 503 instead of acknowledging an event you may lose.
All supported event names
invoice.created, payment.detected, payment.confirming, payment.confirmed, payment.partial, payment.overpaid, invoice.expired, payment.late, payment.review_required, sweep.completed, sweep.failed, webhook.test.
Test events contain message and sent_at; they are not invoice payments. Sweep events describe settlement, not the signal to fulfill an order.
Coinplexo signs the timestamp and the exact raw request body with HMAC-SHA256 and your endpoint’s signing secret.
secret, timestamp + "." + rawBodyCapture the raw bytes before parsing JSON. Re-serializing an object can change its bytes and invalidate the signature. Reject timestamps more than 300 seconds in the past or future, and compare signatures in constant time. Keep your server clock synchronized.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(secret, header, rawBody, now = Date.now()) {
if (!secret || typeof header !== "string" || !Buffer.isBuffer(rawBody)) {
return false;
}
const fields = header.split(",").map((part) => part.trim().split("="));
const timestamps = fields.filter(([key]) => key === "t");
const signatures = fields.filter(([key]) => key === "v1");
if (timestamps.length !== 1 || signatures.length !== 1) return false;
if (timestamps[0].length !== 2 || signatures[0].length !== 2) return false;
const timestamp = timestamps[0][1];
const signature = signatures[0][1];
if (!/^\d+$/.test(timestamp) || !/^[a-fA-F0-9]{64}$/.test(signature)) {
return false;
}
const seconds = Number(timestamp);
if (!Number.isSafeInteger(seconds) || !Number.isFinite(now)) return false;
if (Math.abs(now / 1000 - seconds) > 300) return false;
const expected = createHmac("sha256", secret)
.update(timestamp + ".")
.update(rawBody)
.digest();
return timingSafeEqual(expected, Buffer.from(signature, "hex"));
}Connect verification to your receiver
This Next.js handler shows the receiving flow. Save the verifier above beside your route. Implement inbox.insertOnce using your database: commit a uniquely keyed event record atomically, let duplicates succeed, and process saved events in a worker. This adapter is application code you provide.
// Next.js App Router: app/api/coinplexo/webhook/route.js
import { verifyWebhook } from "./verify-webhook.mjs";
import { inbox } from "@/lib/webhook-inbox"; // Your durable storage adapter
export const runtime = "nodejs";
export async function POST(request) {
const rawBody = Buffer.from(await request.arrayBuffer());
const secret = process.env.COINPLEXO_WEBHOOK_SECRET;
if (!secret) return new Response("Not configured", { status: 503 });
if (!verifyWebhook(secret, request.headers.get("Coinplexo-Signature"), rawBody)) {
return new Response("Invalid signature", { status: 401 });
}
const eventId = request.headers.get("Coinplexo-Event-Id");
const eventType = request.headers.get("Coinplexo-Event-Type");
if (!eventId || !eventType) return new Response("Missing headers", { status: 400 });
let payload;
try { payload = JSON.parse(rawBody.toString("utf8")); }
catch { return new Response("Invalid JSON", { status: 400 }); }
try {
// Atomically persist; duplicate IDs must be a successful no-op.
// A worker processes this inbox and reconciles the invoice before fulfillment.
await inbox.insertOnce({ eventId, eventType, payload });
} catch {
return new Response("Retry later", { status: 503 });
}
return new Response(null, { status: 204 });
}All routes below are relative to your API origin and require a merchant API key. Resources are scoped to the authenticated merchant.
GET/api/v1/assets
List assets on enabled networks. Use the returned asset IDs and decimals.
Scope: Any valid key
GET/api/v1/min-amount
Check order minimums and processing fees. Filters: chain, asset, currency.
Scope: Any valid key
POST/api/v1/invoices
Create an invoice and get a hosted checkout URL. Supports Idempotency-Key.
Scope: invoices:write
GET/api/v1/invoices
List your invoices. Filters: status, asset, q, limit, offset.
Scope: invoices:read
GET/api/v1/invoices/{id}
Read an invoice and its recorded payments.
Scope: invoices:read
GET/api/v1/invoices/{id}/payments
Read the on-chain payments associated with an invoice.
Scope: invoices:read
GET/api/v1/merchants/{id}/transactions
List your merchant’s payments. Supports limit and offset.
Scope: invoices:read
GET/api/v1/merchants/{id}/balances
Read confirmed ledger balances per asset; settlement is tracked separately.
Scope: merchant:read
POST/api/v1/webhooks/test
Queue a signed webhook.test event for matching enabled subscriptions.
Scope: webhooks:test
Bring the API into your tools
Download the OpenAPI 3.1 schema for request fields, response types, and your API client.
Building a custom payment view?
GET /pub/v1/invoices/{id} returns customer-safe payment details. GET /pub/v1/invoices/{id}/events streams live updates using Server-Sent Events. These endpoints need no API key. Use authenticated invoice reads and verified webhooks for your fulfillment decisions.
Errors are JSON objects with error and message fields. Check the HTTP status before reading a response as an invoice.
| HTTP | Meaning | What to do |
|---|---|---|
400 | Invalid JSON or malformed ID | Check the request format. Unknown request fields are rejected. |
401 | Missing or invalid key | Check your backend environment and authentication header. |
403 | Insufficient access | Ask for the needed scope; use your own merchant ID. |
404 | Resource not found | Check the invoice ID and which merchant key created it. |
409 | Conflicting or in-progress request | For an in-progress idempotent request, wait and retry with the same key and body. |
422 | Validation or idempotency mismatch | Read message. Check asset, minimum amount, callback domains, and the original request body. |
429 | Rate limit reached | Back off and inspect the X-RateLimit-* headers. |
5xx | Server failure | Retry with backoff. Reuse the original idempotency key and body when creating an invoice. |
Before enabling payments for customers
Use the test environment and test asset supplied by your operator. Available assets depend on that environment.
- Create an invoice, save its ID, and open the returned checkout URL.
- Retry the same request and verify that it returns the original invoice.
- Receive a signed webhook.test event; verify that it does not fulfill an order.
- Reject an altered body, invalid signature, and an expired timestamp.
- Deliver an event twice and confirm the order is fulfilled only once.
- Exercise successful, partial, expired, and late payments with your operator.