Skip to documentation
Documentation / Merchant integration

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.

  1. 01 / Your serverCreate an invoice

    Lock a quote for the order.

  2. 02 / Your customerPay at checkout

    Scan, send, and follow progress.

  3. 03 / Your backendConfirm the order

    Verify the event before fulfillment.

Before you start

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.

01

Authenticate from your server

#

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.

Connect to the API
# 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"
Request only the scopes your integration needs
ScopeAllows
invoices:writeCreate payments.
invoices:readRead invoices and payments.
merchant:readRead your ledger balances.
webhooks:testSend a signed test event.
02

Create a payment

#

A payment starts with an invoice: an order amount, a selected crypto asset, and a time-limited exchange rate.

POST/api/v1/invoicesinvoices:write
  1. 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.
  2. Check the minimum. Read GET /api/v1/min-amount?asset=USDC_SOL&currency=USD. Minimums and fees are configurable; use the response rather than hard-coding them.
  3. Create the invoice on your backend. Use the order amount from your database and a stable idempotency key for this payment attempt.
Create invoice
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
  }'
Request fields
FieldRequirementWhat to send
assetRequiredAn enabled asset ID allowed for your merchant.
fiat_amountRequiredA positive decimal string, such as "49.00".
external_order_idRecommendedYour order reference, up to 128 characters. Save the returned invoice ID too.
fiat_currencyOptionalFor example, USD. Defaults to your merchant’s currency.
success_url
cancel_url
OptionalYour HTTPS return pages. Ask the operator to allow your callback domains.
ttl_secondsOptionalQuote lifetime: 300–86400 seconds. Default: 3600.
descriptionOptionalCustomer-facing order description, up to 500 characters.
customer_ref
metadata
OptionalA customer reference (128 characters max) and up to 20 metadata keys. Avoid sensitive data.
03

Send the customer to checkout

#

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

201 Created · example response
{
  "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.

A $10 USDT-TRON order
# USDT on TRON, $10 order
GET /api/v1/min-amount?asset=USDT_TRON&currency=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.

How to handle invoice status
StatusMerchant action
CREATED · WAITING_PAYMENTShow the checkout link and wait.
PAYMENT_DETECTED · CONFIRMINGPayment is in progress. Wait for final confirmation.
PARTIALLY_PAIDMore funds are needed; do not fulfill yet.
PAID · OVERPAIDReconcile the invoice with the order and fulfill once. Handle any excess through your support process.
EXPIREDOffer a new invoice. Stop requesting funds to the expired quote.
UNDERPAID · LATE_PAYMENT · MANUAL_REVIEW · FAILEDHold 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.

Retrieve an invoice
curl --fail-with-body \
  "$COINPLEXO_API_URL/api/v1/invoices/$INVOICE_ID" \
  -H "X-API-Key: $COINPLEXO_API_KEY"
04

Set up payment webhooks

#

Receive payment updates at your backend without keeping a browser open or continuously polling.

  1. Deploy a public HTTPS endpoint. For example, https://shop.example.com/api/coinplexo/webhook. It must accept JSON POST requests without a browser login.
  2. 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.
  3. Select events and save the signing secret. Subscribe to payment.confirmed, plus the lifecycle events your app handles. Include webhook.test to receive test deliveries. Store the endpoint’s secret in COINPLEXO_WEBHOOK_SECRET; it is separate from your API key and shown only on creation or rotation.
  4. 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.
Send a signed test event
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>
payment.confirmed · shortened payload
{
  "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.

05

Verify every webhook signature

#

Coinplexo signs the timestamp and the exact raw request body with HMAC-SHA256 and your endpoint’s signing secret.

HMAC-SHA256secret, timestamp + "." + rawBody

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

verify-webhook.mjs · Node.js
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.

Webhook receiver pattern
// 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 });
}
06

Merchant API reference

#

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.

Download schema

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.

07

Handle errors. Test the full flow.

#

Errors are JSON objects with error and message fields. Check the HTTP status before reading a response as an invoice.

Common responses and recovery
HTTPMeaningWhat to do
400Invalid JSON or malformed IDCheck the request format. Unknown request fields are rejected.
401Missing or invalid keyCheck your backend environment and authentication header.
403Insufficient accessAsk for the needed scope; use your own merchant ID.
404Resource not foundCheck the invoice ID and which merchant key created it.
409Conflicting or in-progress requestFor an in-progress idempotent request, wait and retry with the same key and body.
422Validation or idempotency mismatchRead message. Check asset, minimum amount, callback domains, and the original request body.
429Rate limit reachedBack off and inspect the X-RateLimit-* headers.
5xxServer failureRetry 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.