# CyroPay Merchant Invoice API

Base URL: `https://cyropay.io`

## Authentication

Generate a merchant API key in Merchant Service. Keep it on your bot's server, never in a browser or a public repository. All invoice requests require `Authorization: Bearer YOUR_MERCHANT_API_KEY`.

Receiving wallets must be saved in Settings for the requested coin. BTC uses your Bitcoin wallet; SOL, USDT and USDC use your Solana wallet; ETH and POL use your Ethereum wallet; TRX uses your Tron wallet. These invoices forward payments to those wallets; notifications do not credit a CyroPay account or execute a payout.

## Create invoice

`POST /api/public/v1/invoices`

Headers: `Authorization: Bearer YOUR_MERCHANT_API_KEY`, `Content-Type: application/json`

```json
{
  "coin": "SOL",
  "amount_usd": 25,
  "title": "Order 123",
  "order_id": "123",
  "callback_url": "https://your-bot-domain.com/payments/cyropay"
}
```

Supported coins: BTC, SOL, ETH, POL, TRX, USDT, USDC. `amount_usd`: 1–1,000,000. Title and order ID: up to 80 characters. `order_id` and `callback_url` are optional. Callback URLs must resolve to public addresses and use HTTPS on port 443; redirects are not followed.

Success (HTTP 200):

```json
{
  "id": "INVOICE_UUID",
  "status": "waiting",
  "coin": "SOL",
  "address": "PAYMENT_ADDRESS",
  "amount_coin": 0.18,
  "amount_usd": 25,
  "payment_url": "https://cyropay.io/pay/INVOICE_UUID",
  "webhook_secret": "PER_INVOICE_SIGNING_SECRET"
}
```

`payment_url` is a hosted CyroPay payment page for this invoice (amount, address, QR code and live status). Send it to the customer as a link/button, like an OxaPay pay link. Example quantities are illustrative; use the returned live amount. `webhook_secret` appears only when `callback_url` is provided. Save it securely against the invoice ID before processing notifications; it is not returned by polling. Existing invoices without a callback continue to use polling; supply the callback when creating a new invoice.

Errors: 400 invalid input/callback, 401 invalid or revoked key, 409 missing receiving wallet, 502 unavailable price/payment provider, 500 storage failure. Invoice creation has no idempotency guarantee; do not blindly retry an ambiguous response.

## Poll invoice

`GET /api/public/v1/invoices?id=INVOICE_UUID` with the same Authorization header.

Returns `id`, `status`, `coin`, `amount_coin`, `amount_usd`, `address_in`, `txid`, `confirmations`, `created_at`. Status values: `waiting`, `confirming`, `underpaid`, `paid`. A different merchant's invoice returns 404. A confirmed payment within the existing 0.5% amount tolerance is considered paid.

## Payment notifications

CyroPay sends an HTTPS POST to `callback_url` after the payment provider marks the invoice paid. No pending or underpaid event is sent.

```json
{
  "id": "EVENT_UUID",
  "type": "invoice.paid",
  "created_at": "2026-11-01T19:00:00Z",
  "data": {
    "id": "INVOICE_UUID",
    "order_id": "123",
    "status": "paid",
    "coin": "SOL",
    "amount_coin": 0.18,
    "amount_usd": 25,
    "value_received": 0.18,
    "txid": "BLOCKCHAIN_TRANSACTION_ID",
    "confirmations": 1
  }
}
```

Headers:
- `Content-Type: application/json`
- `X-CyroPay-Event-Id`: stable event ID across retries
- `X-CyroPay-Timestamp`: Unix seconds, regenerated for each delivery
- `X-CyroPay-Signature`: `sha256=` followed by lowercase HMAC-SHA256 hex

Verify the signature using the exact raw request body, before trusting any payment fields. Signed text: `timestamp + "." + rawBody`, HMAC key: the invoice's `webhook_secret`. Reject timestamps more than 300 seconds old or in the future. Compare signature bytes in constant time. Look up the expected secret using the invoice ID, then validate the signed invoice against your saved order. Never use your merchant API key as the webhook secret.

Node.js verification example (your bot server):

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyCyroPay(rawBody, timestamp, signature, secret) {
  if (!/^\d+$/.test(timestamp ?? '') || !/^sha256=[0-9a-f]{64}$/.test(signature ?? '')) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > 300) return false;
  const expected = createHmac('sha256', secret).update(timestamp + '.').update(rawBody).digest();
  const received = Buffer.from(signature.slice(7), 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}
```

After verifying, atomically save the event ID and apply the order credit exactly once. Return HTTP 200–299 within four seconds after durable processing (or after safely queuing the event). Return 2xx for an already processed event. Deduplicate using the signed body's `id`, and verify it matches `X-CyroPay-Event-Id`. Do not credit users based solely on an unverified request.

Delivery is at least once, not exactly once. Failed deliveries receive up to ten total attempts, with increasing delays from one minute up to one hour; retry jobs run each minute. Delivery may be delayed; keep polling as a reconciliation fallback. Changing your endpoint does not update callbacks on existing invoices. Notifications do not contain your merchant API key, the signing secret, or payer personal details.