> ## Documentation Index
> Fetch the complete documentation index at: https://www.niftipay.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Crypto Orders

> List, create, fetch, update, delete, and refund crypto invoice orders (payment URI + QR, status lifecycle, and merchant webhooks).

# Orders API

This document covers the **Orders** endpoints used to create and manage **crypto invoice orders** (deposit address + payment URI + QR + lifecycle status), including **refunds** for cancelled orders.

It includes:

* List orders (`GET /api/orders`)
* Create an invoice order (`POST /api/orders`)
* Delete an order by reference (`DELETE /api/orders?reference=...`)
* Fetch a single order + payment notifications (`GET /api/orders/:orderId`)
* Update an order status (`PATCH /api/orders/:orderId`) — including webhook dispatch + forward funds
* Refund a cancelled order (`POST /api/orders/:orderId/refund`)

> These endpoints are protected, you must be authenticated.

***

## Base URL

All examples use:

* `https://www.niftipay.com`

If you're running locally, replace with your own base URL.

***

## Authentication

These endpoints support **two authentication methods** (depending on your integration setup):

### 1) API Key (recommended for server-to-server integrations)

Send your API key in the `x-api-key` header.

```bash theme={null}
curl -X GET "https://www.niftipay.com/api/orders" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json"
```

### 2) Session cookie (browser / dashboard usage)

If you are authenticated via the dashboard..

> For browser calls, you usually don't need to add headers manually — the cookie is sent automatically.

***

## Order statuses

Crypto invoice orders use these statuses:

* `pending` — waiting for payment (default on creation)
* `paid` — fully paid (or manually marked paid)
* `cancelled` — cancelled (manual or automated)
* `refunded` — refund executed and wallet freed

***

## Chain + asset rules

`network` and `asset` work like this:

* `network` is the chain where the deposit address lives: `BTC | LTC | ETH | SOL | XRP`
* `asset` is what the customer sends:
  * Native coin (e.g. `BTC`, `ETH`, `SOL`)
  * Or an ERC‑20 token on Ethereum for supported tokens (e.g. `USDT`, `USDC`) **only when `network` is `ETH`**

When the API returns amounts, it may include:

* `amount` — base invoice amount
* `networkFees` — any network fee the customer must add (nullable)
* `totalToSend` — `amount + networkFees` (or just `amount` if fees are null)

***

## Payment URI + QR URL

For convenience, orders return:

* `paymentUri` — a standard chain-specific URI (bitcoin:, ethereum:, solana:, ripple:, etc.)
* `qrUrl` — a QR image URL generated from `paymentUri`

Example formats:

* BTC: `bitcoin:<address>?amount=<totalToSend>`
* LTC: `litecoin:<address>?amount=<totalToSend>`
* SOL: `solana:<address>?amount=<totalToSend>`
* XRP: `ripple:<address>?amount=<totalToSend>&dt=<destinationTag?>`
* ETH native: `ethereum:<address>?amount=<totalToSend>`
* ETH token: `ethereum:<address>?contract=<tokenContract>&amount=<totalToSend>`

***

# List Orders

## Endpoint

`GET /api/orders`

Returns a list of orders for the authenticated user, newest first.

### Query parameters

| Name        | Type   | Description                                                               |
| ----------- | ------ | ------------------------------------------------------------------------- |
| `status`    | string | Filter by order status (e.g. `pending`, `paid`, `cancelled`, `refunded`). |
| `reference` | string | Filters orders where `reference` starts with this value.                  |
| `search`    | string | Searches in `firstName`, `lastName`, and `email`.                         |

### Example request: list all orders

```bash theme={null}
curl -X GET "https://www.niftipay.com/api/orders" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json"
```

### Example request: filter by status

```bash theme={null}
curl -X GET "https://www.niftipay.com/api/orders?status=pending" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json"
```

### Example request: filter by reference prefix

```bash theme={null}
curl -X GET "https://www.niftipay.com/api/orders?reference=INV-" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json"
```

### Example request: search by customer name/email

```bash theme={null}
curl -X GET "https://www.niftipay.com/api/orders?search=john" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json"
```

### Example response

```json theme={null}
{
  "orders": [
    {
      "id": "a9a3c588-9a6c-4e44-9b21-5c8d1f4f2b91",
      "reference": "INV-10001",
      "network": "ETH",
      "asset": "USDT",
      "amount": "68",
      "networkFees": "0.5",
      "totalToSend": "68.5",
      "currency": "EUR",
      "firstName": "John",
      "lastName": "Smith",
      "email": "john@example.com",
      "addressId": "a6a71a4c-1f6d-4c56-b3a2-3a52dbe8b3d3",
      "destinationTag": null,
      "status": "pending",
      "createdAt": "2026-02-11T10:00:00.000Z",
      "expiresAt": "2026-02-11T22:00:00.000Z",
      "paidAt": null,
      "txId": null,
      "blockNumber": null
    }
  ]
}
```

***

# Create Order (Crypto Invoice)

## Endpoint

`POST /api/orders`

Creates a **crypto invoice order**:

* reserves a deposit address
* computes the amount in crypto/token based on your fiat amount
* returns `paymentUri` and `qrUrl` for checkout UX
* starts in `pending`

### Important behavior

* **Payment method gating**: if crypto invoices are disabled for your user, this endpoint returns `403`.
* **Reference handling**:
  * If you provide a `reference`, it must be unique for your user.
  * If you don't provide a `reference`, one is generated (UUID).
  * If you provide a `reference` that conflicts with a **fiat** order `merchantReference`, it returns `409`.
* **Idempotency (optional)**:
  * If you pass `Idempotency-Key` (or `x-idempotency-key`) and you retry with the same `reference`,
    the API can return the **existing** order instead of failing.
* **Replace cancelled (optional)**:
  * Add `?replaceCancelled=1` to allow re-using a reference only when the prior order is `cancelled`
    (and the old order is safely deletable).

### Request body

```json theme={null}
{
  "network": "ETH",
  "asset": "USDT",
  "amount": 68,
  "currency": "EUR",
  "firstName": "John",
  "lastName": "Smith",
  "email": "john@example.com",
  "reference": "INV-10001",
  "merchantId": "optional-merchant-id",
  "pct": "optional-fee-percent-or-flag"
}
```

**Fields**

| Field        | Type          | Required | Notes                                                          |
| ------------ | ------------- | -------- | -------------------------------------------------------------- |
| `network`    | string        | ✅        | One of `BTC`, `LTC`, `ETH`, `SOL`, `XRP`.                      |
| `asset`      | string        | ✅        | Asset symbol. For `USDT/USDC` you must use `network=ETH`.      |
| `amount`     | number        | ✅        | Fiat invoice amount.                                           |
| `currency`   | string        | ❌        | Defaults to `EUR`.                                             |
| `firstName`  | string        | ✅        | Customer name.                                                 |
| `lastName`   | string        | ✅        | Customer name.                                                 |
| `email`      | string        | ✅        | Used for receipts and internal processing.                     |
| `reference`  | string        | ❌        | If omitted, a UUID is generated. Must be unique for this user. |
| `merchantId` | string        | ❌        | Your internal merchant identifier (if you use multi-merchant). |
| `pct`        | string/number | ❌        | Passed through to invoice creation logic.                      |

***

## Example: create an ETH USDT invoice

```bash theme={null}
curl -X POST "https://www.niftipay.com/api/orders" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "ETH",
    "asset": "USDT",
    "amount": 68,
    "currency": "EUR",
    "firstName": "John",
    "lastName": "Smith",
    "email": "john@example.com",
    "reference": "INV-10001"
  }'
```

### Example response (201)

```json theme={null}
{
  "order": {
    "id": "a9a3c588-9a6c-4e44-9b21-5c8d1f4f2b91",
    "reference": "INV-10001",
    "network": "ETH",
    "asset": "USDT",
    "amount": "68",
    "networkFees": "0.5",
    "totalToSend": "68.5",
    "currency": "EUR",
    "firstName": "John",
    "lastName": "Smith",
    "email": "john@example.com",
    "address": "0xabc123...depositAddress",
    "destinationTag": null,
    "paymentUri": "ethereum:0xabc123...depositAddress?contract=0xdAC17F958D2ee523a2206206994597C13D831ec7&amount=68.5",
    "qrUrl": "https://api.qrserver.com/v1/create-qr-code?size=300x300&data=ethereum%3A0xabc123...depositAddress%3Fcontract%3D0xdAC17F958D2ee523a2206206994597C13D831ec7%26amount%3D68.5",
    "status": "pending",
    "createdAt": "2026-02-11T10:00:00.000Z",
    "expiresAt": "2026-02-11T22:00:00.000Z",
    "merchantId": null
  }
}
```

***

## Idempotency example

If you're creating orders from a backend and might retry on network failure, do:

* Include a stable `reference`
* Include an idempotency key

```bash theme={null}
curl -X POST "https://www.niftipay.com/api/orders" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Idempotency-Key: 8f0c8c92-8c4f-4e66-9f8f-5e6f98d2817a" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "BTC",
    "asset": "BTC",
    "amount": 25,
    "currency": "EUR",
    "firstName": "John",
    "lastName": "Smith",
    "email": "john@example.com",
    "reference": "INV-RETRY-1"
  }'
```

If the same `reference` already exists and idempotency headers are present, the API returns the existing order (200) instead of a 409 conflict.

***

## Replace cancelled example

If you want to re-use a reference that exists *only* because the prior invoice was cancelled:

```bash theme={null}
curl -X POST "https://www.niftipay.com/api/orders?replaceCancelled=1" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "network": "ETH",
    "asset": "ETH",
    "amount": 10,
    "currency": "EUR",
    "firstName": "John",
    "lastName": "Smith",
    "email": "john@example.com",
    "reference": "INV-CANCELLED-1"
  }'
```

***

# Delete Order (by reference)

## Endpoint

`DELETE /api/orders?reference=<reference>`

Deletes a crypto invoice order **by reference**, if it is safe to delete.

### When deletion is blocked

The API checks wallet balances and blocks deletion if the deposit wallet has **any funds**.

It also deletes:

* per-order subscriptions (best effort)
* `paymentNotification` rows
* frees the deposit wallet (`isAssigned=false`)
* deletes the order row

### Example request

```bash theme={null}
curl -X DELETE "https://www.niftipay.com/api/orders?reference=INV-10001" \
  -H "x-api-key: YOUR_API_KEY"
```

### Success response

* `204 No Content`

### Error responses

**Missing reference**

```json theme={null}
{ "error": "reference query-param required" }
```

**Not found**

```json theme={null}
{ "error": "Order not found" }
```

**Wallet has balance (cannot delete)**

```json theme={null}
{ "error": "Cannot delete – wallet has 0.01 BTC" }
```

***

# Fetch Order (by ID) + Payments

## Endpoint

`GET /api/orders/:orderId`

Returns:

* the order
* payment notifications (`payments`)
* a computed `runningTotal` (sum of payment notifications)
* a computed `remaining` on-chain balance (best effort)
* a normalized `fees` object (snapshot)

### Example request

```bash theme={null}
curl -X GET "https://www.niftipay.com/api/orders/a9a3c588-9a6c-4e44-9b21-5c8d1f4f2b91" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json"
```

### Example response

```json theme={null}
{
  "order": {
    "id": "a9a3c588-9a6c-4e44-9b21-5c8d1f4f2b91",
    "reference": "INV-10001",
    "userId": "user-id",
    "merchantId": null,
    "chain": "ETH",
    "asset": "USDT",
    "amount": "68",
    "networkFees": "0.5",
    "totalToSend": "68.5",
    "currency": "EUR",
    "firstName": "John",
    "lastName": "Smith",
    "email": "john@example.com",
    "addressId": "a6a71a4c-1f6d-4c56-b3a2-3a52dbe8b3d3",
    "depositAddress": "0xabc123...depositAddress",
    "destinationTag": null,
    "status": "pending",
    "createdAt": "2026-02-11T10:00:00.000Z",
    "expiresAt": "2026-02-11T22:00:00.000Z",
    "paidAt": null,

    "runningTotal": "0",
    "remaining": "0",
    "paymentUri": "ethereum:0xabc123...depositAddress?contract=0xdAC17F958D2ee523a2206206994597C13D831ec7&amount=68.5",
    "qrUrl": "https://api.qrserver.com/v1/create-qr-code?size=300x300&data=...",
    "fees": {
      "snapshotVersion": 1,
      "snapshotAt": "2026-02-11T10:00:00.000Z",
      "platformFeePercent": 2.5,
      "platformFeeAmount": "1.7",
      "merchantNetAmount": "66.3",
      "customerPayAmount": "68.5"
    }
  },
  "payments": [
    {
      "amount": "10",
      "txId": "0xdeadbeef...",
      "blockNumber": "19876543",
      "createdAt": "2026-02-11T10:02:00.000Z",
      "counterAddress": "0xsender..."
    }
  ]
}
```

> Notes:
>
> * `payments` are returned newest first.
> * `runningTotal` is the sum of positive `paymentNotification.amount`.
> * `remaining` is a best-effort on-chain balance read for the deposit wallet.

***

# Update Order Status (manual)

## Endpoint

`PATCH /api/orders/:orderId`

Allows manual transitions between statuses.

Valid statuses:

* `pending`
* `paid`
* `cancelled`

### Request body

```json theme={null}
{
  "status": "paid"
}
```

| Field    | Type   | Required | Notes                                          |
| -------- | ------ | -------- | ---------------------------------------------- |
| `status` | string | ✅        | Must be one of `pending`, `paid`, `cancelled`. |

***

## Important behavior

### Idempotency for status changes

If you PATCH the same status the order already has, the API returns success and a warning, and performs **no** side effects:

* no webhook dispatch
* no forward funds
* no subscription changes

Example response:

```json theme={null}
{
  "success": true,
  "warnings": [
    {
      "code": "no_change",
      "message": "Order is already \"paid\". No action taken."
    }
  ]
}
```

### Side effects by status

#### If status = `paid`

* sets `paidAt = now`
* sends a merchant webhook event `"paid"` (best effort)
* attempts to forward funds for non-XRP invoices (best effort)
* attempts to delete the per-order subscription (best effort)

If forwarding fails, the order stays `paid` and you get a warning:

```json theme={null}
{
  "success": true,
  "warnings": [
    {
      "code": "forward_funds_failed",
      "message": "Funds could not be forwarded (likely dust/fee or zero balance). Payment is marked paid and webhook was sent."
    }
  ]
}
```

If the order is XRP, forwarding is intentionally skipped (destination-tag model):

```json theme={null}
{
  "success": true,
  "warnings": [
    {
      "code": "forward_skipped_xrp",
      "message": "Forwarding is skipped for XRP destination-tag invoices."
    }
  ]
}
```

#### If status = `pending`

* clears `paidAt` and `refundedAt`
* sets a new `expiresAt = now + 12 hours`
* ensures deposit wallet is assigned (`isAssigned=true`)
* attempts to recreate a subscription (best effort, non-XRP)
* sends a merchant webhook event `"pending"` (best effort)

> **Note:** Crypto invoices created through the [onramping flow](/api/onramping) (e.g. Banxa) may have their `expiresAt` automatically extended up to **72 hours from the ramping order's creation time** if KYC is still in progress. See [Banxa KYC timeout behavior](/api/webhooks#banxa-kyc-timeout-behavior).

#### If status = `cancelled`

* attempts to delete subscription (non-XRP)
* checks balance, and if balance is `"0"` it frees the wallet (`isAssigned=false`)
* sends a merchant webhook event `"cancelled"` (best effort)

***

## Example: mark an order as paid

```bash theme={null}
curl -X PATCH "https://www.niftipay.com/api/orders/a9a3c588-9a6c-4e44-9b21-5c8d1f4f2b91" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{ "status": "paid" }'
```

### Example response

```json theme={null}
{ "success": true, "warnings": [] }
```

***

## Example: revert an order to pending

```bash theme={null}
curl -X PATCH "https://www.niftipay.com/api/orders/a9a3c588-9a6c-4e44-9b21-5c8d1f4f2b91" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{ "status": "pending" }'
```

***

## Example: cancel an order

```bash theme={null}
curl -X PATCH "https://www.niftipay.com/api/orders/a9a3c588-9a6c-4e44-9b21-5c8d1f4f2b91" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{ "status": "cancelled" }'
```

***

# Refunds

Refunds are performed **from the deposit wallet** back to a user-provided `refundAddress`.

Refunds are only allowed when:

1. the order exists and belongs to you
2. the order is **cancelled**
3. there is a non-zero balance to refund

After a successful refund:

* order status becomes `refunded`
* `refundedAt` is set
* the deposit wallet is freed (`isAssigned=false`)
* a merchant webhook event `"refunded"` is sent (best effort)

***

## Refund an Order

### Endpoint

`POST /api/orders/:orderId/refund`

### Request body

```json theme={null}
{
  "refundAddress": "YOUR_WALLET_ADDRESS"
}
```

| Field           | Type   | Required | Notes                                              |
| --------------- | ------ | -------- | -------------------------------------------------- |
| `refundAddress` | string | ✅        | Destination address/account to receive the refund. |

***

## Example: refund a cancelled ETH order

```bash theme={null}
curl -X POST "https://www.niftipay.com/api/orders/a9a3c588-9a6c-4e44-9b21-5c8d1f4f2b91/refund" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{ "refundAddress": "0xYourRefundAddressHere" }'
```

### Example success response

```json theme={null}
{
  "success": true,
  "order": {
    "id": "a9a3c588-9a6c-4e44-9b21-5c8d1f4f2b91",
    "reference": "INV-10001",
    "status": "refunded",
    "refundedAt": "2026-02-11T11:00:00.000Z",
    "chain": "ETH",
    "asset": "USDT"
  }
}
```

> The returned `order` is the order row after it was updated to `refunded`.

***

## Refund rules by chain/asset (high level)

The refund implementation attempts to send a **net amount** (after accounting for chain rules/fees) from the deposit wallet:

* **ERC‑20 on ETH (USDT/USDC)**:
  * token amount is refunded in full
  * ETH is required in the deposit wallet to pay gas
  * the system may top-up ETH gas automatically (internal) to enable the refund
* **Native ETH**:
  * refunds `balance - gasFee` (fails if too small after fees)
* **BTC / LTC**:
  * refunds `balance - estimatedFee` (and may broadcast via txData if needed)
  * very small amounts may not refund if below dust/fee thresholds
* **SOL**:
  * keeps a small reserve (rent + fee) and refunds the remainder
* **XRP**:
  * keeps the account reserve (e.g. 10 XRP) and refunds the remainder

***

## Error responses

### Missing refund address (400)

```json theme={null}
{ "error": "Refund address is required" }
```

### Not found (404)

```json theme={null}
{ "error": "Not found" }
```

### Order is not cancelled (400)

```json theme={null}
{ "error": "Refund allowed only for cancelled orders" }
```

### Nothing to refund (400)

```json theme={null}
{ "error": "Nothing to refund" }
```

### Refund failed (502)

```json theme={null}
{ "error": "Refund failed – see logs" }
```

***

# Webhooks (merchant notifications)

When you manually transition status via `PATCH /api/orders/:orderId`, the API attempts to notify your webhook endpoint (best effort) with:

* `"pending"`
* `"paid"`
* `"cancelled"`

When a refund succeeds via `POST /api/orders/:orderId/refund`, the API also sends:

* `"refunded"`

Refund webhook payload includes:

* `id`, `reference`, `merchantId`
* `refundedAt`
* `amount` (the **actual net amount sent** on-chain)
* `chain`, `asset`

> If webhook dispatch fails, the API still completes the operation and returns success.

***

## Node.js Example

A complete Node.js example showing how to create a crypto invoice order, including inline comments for every field and response parsing.

```javascript theme={null}
const order = await apiRequest("POST", "/api/orders", {
  // Blockchain network: "BTC" | "LTC" | "ETH" | "SOL" | "XRP"
  network: "ETH",

  // Crypto asset: native coins or ERC-20 tokens (USDT, USDC on ETH only)
  asset: "USDT",

  // Invoice amount in fiat — converted to crypto automatically
  amount: 50,

  // Customer details
  firstName: "Jane",
  lastName: "Doe",
  email: "jane.doe@example.com",

  // Fiat currency (defaults to "EUR")
  currency: "EUR",

  // Your unique reference (optional — UUID generated if omitted)
  reference: "MY-ORDER-001",
});

// Key response fields:
// order.order.id            — Niftipay order ID
// order.order.address       — blockchain deposit address
// order.order.totalToSend   — crypto amount to send (amount + network fees)
// order.order.paymentUri    — wallet-compatible payment URI
// order.order.qrUrl         — QR code image URL
// order.order.status        — "pending" initially
// order.order.expiresAt     — invoice expiration time
```

> See the full working script with setup and helper function: [API Examples (Node.js)](/api/examples#example-1-create-a-crypto-invoice-order)

***

# Error codes summary

These endpoints commonly return:

* `400` — invalid payload, invalid status, missing required query param, nothing to refund
* `403` — payment method disabled (crypto invoices disabled)
* `404` — order not found
* `409` — reference conflict, wallet has balance, no available addresses
* `502` — refund broadcast failed (upstream/provider errors)

***
