# NextCryptoPay — AI Agent Integration Guide

> **This document is written for AI coding agents** (Claude Code, Codex, Cursor, or any LLM-based assistant).
> If you are an AI agent: your task is to integrate NextCryptoPay — a **non-custodial crypto payment gateway** — into your user's project. Everything you need is in this file: endpoints, schemas, webhook verification, error codes, and a step-by-step checklist. Follow it exactly; do not invent fields or endpoints that are not documented here.
> If you are a human: download this file and paste it into your AI agent's context (or point the agent at `https://nextcryptopay.com/docs-agent.md`).

- **Base URL:** `https://nextcryptopay.com`
- **Human docs:** `https://nextcryptopay.com/docs`
- **Machine index:** `https://nextcryptopay.com/llms.txt`
- **Support:** `https://nextcryptopay.com/contact`

---

## 1. What NextCryptoPay is (30 seconds)

NextCryptoPay lets a merchant accept crypto payments (USDT, USDC, ETH-chain assets, BTC) **directly into their own wallet**. It is **non-custodial**: NextCryptoPay never holds funds. The merchant configures an xpub (a fresh address is derived per invoice) or a static address (a unique payment amount is used to match each invoice). A blockchain watcher confirms payment on-chain and notifies the merchant's server by signed webhook.

Supported chain / asset matrix (exact values for the `chain` and `asset` fields):

| `chain`    | valid `asset` values |
|------------|----------------------|
| `tron`     | `USDT`, `USDC`       |
| `ethereum` | `USDT`, `USDC`       |
| `bsc`      | `USDT`               |
| `bitcoin`  | `BTC`                |

Any other combination returns `400 CHAIN_ASSET_INVALID`.

## 2. Prerequisites (tell your user to do these in the dashboard)

1. Create an account at `https://nextcryptopay.com/register` and a **Shop**.
2. Connect a wallet in **Dashboard → Wallets** (xpub recommended; static address also supported). Without a wallet for the target chain, invoice creation returns `400 NO_WALLET`.
3. Create an API key in **Dashboard → API Keys**. Format: `sk_live_<48 hex>`. It is shown **once** — store it as an environment variable (e.g. `NEXTCRYPTOPAY_API_KEY`). Never commit it or expose it to the browser.
4. (Recommended) Set a default **Webhook URL + secret** in **Dashboard → Webhooks**. The webhook secret is used to verify signatures (§6).

## 3. The integration flow (happy path)

```
Your server                          NextCryptoPay                    Customer
    │  POST /api/v1/invoices              │                               │
    │─────────────────────────────────────▶                               │
    │  201 { id, checkoutUrl, ... }       │                               │
    ◀─────────────────────────────────────│                               │
    │  redirect customer to checkoutUrl   │                               │
    │──────────────────────────────────────────────────────────────────────▶
    │                                     │   customer pays on-chain      │
    │  POST webhook invoice.paid (signed) │◀──────────────────────────────│
    ◀─────────────────────────────────────│                               │
    │  verify signature → fulfill order   │                               │
```

You only need **one server-side call** (create invoice) and **one webhook handler** (fulfill on `invoice.paid`). The hosted checkout page (`checkoutUrl`) handles QR code, countdown, live status, and redirect back to the merchant.

## 4. API reference

Authentication for invoice creation: HTTP header `Authorization: Bearer sk_live_...`.

### 4.1 Create invoice — `POST /api/v1/invoices`

Headers:

| Header            | Required | Notes |
|-------------------|----------|-------|
| `Authorization`   | yes      | `Bearer sk_live_...` |
| `Content-Type`    | yes      | `application/json` |
| `Idempotency-Key` | no       | Any unique string per logical attempt. Safe retries: the same key returns the stored response verbatim. |

Body:

| Field         | Type   | Required | Notes |
|---------------|--------|----------|-------|
| `orderId`     | string | yes      | Your order reference, 1–80 chars. **Unique per shop** — reusing an `orderId` returns the existing invoice (HTTP 200) instead of creating a new one. |
| `amount`      | number | yes      | Price in fiat, > 0. Example: `99.50`. |
| `currency`    | string | no       | Fiat ISO code. Default `"USD"`. Others (e.g. `"EUR"`, `"VND"`) are converted with live rates. |
| `chain`       | string | no       | `tron` \| `ethereum` \| `bsc` \| `bitcoin`. Default `tron`. |
| `asset`       | string | no       | Default `USDT`. Must be valid for `chain` (see §1). |
| `webhookUrl`  | string | no       | HTTPS URL for this invoice's webhooks. If omitted, the shop's default webhook URL is used. Internal/private-network URLs are rejected (`UNSAFE_URL`). |
| `redirectUrl` | string | no       | Where the hosted checkout sends the customer after payment. http(s) only. |

Success — `201 Created` (or `200` when `orderId` already exists):

```json
{
  "id": "inv_abc123",
  "orderId": "ORDER-1001",
  "payAddress": "TJ9xH4n2k8sQw7Lm3vR5pZ1aB6cD8eF2g",
  "cryptoAmount": "99.50",
  "asset": "USDT",
  "chain": "tron",
  "status": "PENDING",
  "expiresAt": "2026-07-30T10:15:00.000Z",
  "checkoutUrl": "https://nextcryptopay.com/pay/inv_abc123"
}
```

Notes for the agent:
- `cryptoAmount` is a **string** (decimal precision matters — never parse to float for display or comparison).
- **The customer must send exactly `cryptoAmount`** — display it verbatim, never round it. When the merchant's wallet is configured as a single static address, that exact amount is the only thing distinguishing this invoice from other open ones, so an underpayment or overpayment does **not** settle the invoice; it is recorded for manual reconciliation and the invoice still expires. (With an xpub wallet each invoice gets its own address, so small overpayments still settle.) Warn the user if they plan to let customers pay from an exchange account — exchanges deduct withdrawal fees and the arriving amount will not match.
- Invoices expire after **15 minutes** by default (`expiresAt`).
- Store `id` and `orderId` in your database, then redirect the customer to `checkoutUrl`.

Errors:

| HTTP | `error` code           | Meaning / what to do |
|------|------------------------|----------------------|
| 400  | `INVALID_INPUT`        | Body failed validation — check field types and lengths. |
| 400  | `CHAIN_ASSET_INVALID`  | Asset not available on that chain (§1). |
| 400  | `NO_WALLET`            | Merchant has no wallet configured for this chain — tell the user to add one in Dashboard → Wallets. |
| 401  | `UNAUTHENTICATED`      | Missing/invalid API key. |
| 404  | `SHOP_NOT_FOUND`       | Session auth with an invalid `shopId`. |
| 409  | `INSUFFICIENT_BALANCE` | Merchant's fee balance/quota exhausted — top up in Dashboard → Billing. |
| 429  | `RATE_LIMITED`         | Limit: **60 invoices/min per shop**. Respect the `retry-after` response header. |
| 503  | `NO_AMOUNT_SLOT`       | Static-address wallets only: too many open invoices share one address, so no unique amount is free. Retry after some invoices settle or expire. |

### 4.2 Get invoice — `GET /api/v1/invoices/{id}`

Public (no auth). Returns live status; the hosted checkout uses this too.

```json
{
  "id": "inv_abc123",
  "orderId": "ORDER-1001",
  "merchantName": "My Shop",
  "fiatAmount": 99.5,
  "fiatCurrency": "USD",
  "chain": "tron",
  "chainLabel": "Tron (TRC20)",
  "asset": "USDT",
  "cryptoAmount": "99.50",
  "payAddress": "TJ9x…",
  "requiredConfirmations": 20,
  "status": "CONFIRMING",
  "confirmations": 7,
  "secondsLeft": 512,
  "redirectUrl": "https://yourstore.com/thanks"
}
```

`404` → `{ "error": "NOT_FOUND" }`.

### 4.3 Live status stream (SSE) — `GET /api/v1/invoices/{id}/stream`

Server-Sent Events; one JSON object per event, roughly every 3 s:

```
data: {"status":"CONFIRMING","confirmations":7,"secondsLeft":512}
```

The stream closes automatically once the status is final (`PAID`, `EXPIRED`, `CANCELLED`, `FAILED`). Use this for custom checkout UIs; if you use the hosted `checkoutUrl` you do not need it.

## 5. Invoice statuses

| Status           | Final? | Meaning |
|------------------|--------|---------|
| `NEW`            | no     | Created, not yet active. |
| `PENDING`        | no     | Waiting for the customer to pay. |
| `CONFIRMING`     | no     | Payment seen on-chain, waiting for confirmations. |
| `PARTIALLY_PAID` | no     | Less than the full amount received so far. |
| `PAID`           | yes    | Fully paid and confirmed. **Fulfill the order only on this status.** |
| `EXPIRED`        | yes    | Not (fully) paid before `expiresAt`. |
| `FAILED`         | yes    | Processing failure. |
| `CANCELLED`      | yes    | Cancelled by the merchant. |

## 6. Webhooks — receive and VERIFY

Webhook events: `invoice.confirming`, `invoice.paid`, `invoice.expired` (plus `invoice.updated` when a merchant manually re-sends from the dashboard).

Delivery is `POST` with headers:

| Header                  | Meaning |
|-------------------------|---------|
| `x-cryptopay-event`     | Event name, e.g. `invoice.paid`. |
| `x-cryptopay-delivery`  | **Stable delivery id** — the same event retried keeps the same id. Use it to deduplicate. |
| `x-cryptopay-timestamp` | Unix seconds when the delivery was signed. |
| `x-cryptopay-signature` | Hex HMAC-SHA256 of `"{timestamp}.{rawBody}"` using your **webhook secret** (Dashboard → Webhooks). |

Payload:

```json
{
  "event": "invoice.paid",
  "invoice": {
    "id": "inv_abc123",
    "orderId": "ORDER-1001",
    "status": "PAID",
    "chain": "tron",
    "asset": "USDT",
    "amount": "99.50",
    "paidAmount": "99.50",
    "payAddress": "TJ9x…"
  }
}
```

Retry policy: up to **6 attempts** with backoff **1, 5, 30, 120, 360, 1440 minutes**; request timeout **15 s**; any `2xx` response marks the delivery as successful; redirects are **not** followed.

**Verification is mandatory.** The signature is computed over the **raw request body** — read the raw bytes *before* any JSON parsing. Reference implementation (Node.js):

```js
import crypto from "node:crypto";

function verifyCryptoPayWebhook(rawBody, headers, secret) {
  const ts = headers["x-cryptopay-timestamp"];
  const sig = headers["x-cryptopay-signature"];
  if (!ts || !sig) return false;

  // 1) Replay protection: reject if older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  // 2) Timing-safe HMAC comparison over "timestamp.rawBody".
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");
  if (sig.length !== expected.length) return false;
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
```

Express example (note `express.raw` — do NOT use `express.json` on this route):

```js
app.post("/webhooks/cryptopay", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyCryptoPayWebhook(raw, req.headers, process.env.CRYPTOPAY_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  const deliveryId = req.headers["x-cryptopay-delivery"];
  if (alreadyProcessed(deliveryId)) return res.status(200).end(); // dedupe

  const { event, invoice } = JSON.parse(raw);
  if (event === "invoice.paid") fulfillOrder(invoice.orderId); // idempotent!
  res.status(200).end(); // reply 2xx fast; do heavy work async
});
```

Next.js App Router example:

```ts
// app/api/webhooks/cryptopay/route.ts
export async function POST(req: Request) {
  const raw = await req.text(); // raw body BEFORE parsing
  const h = Object.fromEntries(req.headers);
  if (!verifyCryptoPayWebhook(raw, h, process.env.CRYPTOPAY_WEBHOOK_SECRET!)) {
    return new Response(null, { status: 401 });
  }
  const { event, invoice } = JSON.parse(raw);
  if (event === "invoice.paid") await fulfillOrder(invoice.orderId);
  return new Response(null, { status: 200 });
}
```

## 7. Minimal server-side example (create invoice)

```js
const res = await fetch("https://nextcryptopay.com/api/v1/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NEXTCRYPTOPAY_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `order-1001-attempt-1`,
  },
  body: JSON.stringify({
    orderId: "ORDER-1001",
    amount: 99.5,
    currency: "USD",
    chain: "tron",
    asset: "USDT",
    webhookUrl: "https://yourstore.com/webhooks/cryptopay",
    redirectUrl: "https://yourstore.com/thanks?order=ORDER-1001",
  }),
});
const invoice = await res.json();
if (!res.ok) throw new Error(invoice.error);
// → redirect the customer to invoice.checkoutUrl
```

## 8. Integration checklist for the AI agent

Work through these in order; all must hold before you tell your user the integration is done:

1. [ ] API key read from an **environment variable**, used **server-side only** (never shipped to the browser, never committed).
2. [ ] Invoice created server-side with a **unique `orderId`** derived from your user's order table, and an `Idempotency-Key` on retries.
3. [ ] `id`, `orderId`, `status`, and `checkoutUrl` persisted before redirecting the customer.
4. [ ] Customer redirected to `checkoutUrl` (do not rebuild the payment UI unless the user explicitly wants a custom checkout — then use §4.2/§4.3).
5. [ ] Webhook endpoint implemented with **raw-body signature verification** (§6), timestamp check, and **dedupe by `x-cryptopay-delivery`**.
6. [ ] Order fulfillment happens **only** on a verified `invoice.paid` webhook (or a polled `status === "PAID"`) — never on the redirect alone: a customer returning to `redirectUrl` proves nothing.
7. [ ] Fulfillment is **idempotent** (webhooks can be retried and manually re-sent).
8. [ ] `EXPIRED` / `PARTIALLY_PAID` handled: show the customer a retry path (create a new invoice with a **new** `orderId`).
9. [ ] `429` handled with backoff honoring `retry-after`.
10. [ ] Webhook secret stored in an environment variable (e.g. `CRYPTOPAY_WEBHOOK_SECRET`).

## 9. Things you must NOT do

- Do **not** poll `GET /api/v1/invoices/{id}` more than ~1 req/3s per invoice — use the SSE stream or webhooks.
- Do **not** compare `cryptoAmount` as a float; treat amounts as decimal strings.
- Do **not** fulfill based on `redirectUrl` hits or client-side state.
- Do **not** skip signature verification "temporarily" — an unverified webhook endpoint is an order-for-free endpoint.
- Do **not** invent endpoints/fields that are not in this document; if something is missing, direct your user to `https://nextcryptopay.com/contact`.

---

*Last verified against the live API surface: 2026-07-30. Canonical URL of this file: `https://nextcryptopay.com/docs-agent.md`.*
