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

# Fiat Orders

> Create and manage fiat card orders, including pricing breakdown, cancellation, deletion rules, and refunds.

# Fiat Orders

Fiat orders are **card payments** processed by us.

This page covers:

* List fiat orders (`GET /api/fiat/orders`) — cursor pagination
* Create a fiat order (`POST /api/fiat/orders`) — returns `payUrl` + `qrUrl`
* Delete a fiat order by reference (`DELETE /api/fiat/orders?reference=...`) — proxy-friendly safety delete
* Fetch a fiat order by `orderKey` (`GET /api/fiat/orders/:orderKey`) — includes pricing + fee snapshot
* Cancel a fiat order by `orderKey` (`DELETE /api/fiat/orders/:orderKey`) — cancels upstream PSP order
* Cancel a fiat order by reference (`DELETE /api/fiat/orders/cancel?reference=...`)
* List refunds (`GET /api/fiat/orders/:orderKey/refunds`)
* Create a refund (`POST /api/fiat/orders/:orderKey/refunds`) — supports partial/multiple refunds

***

## Base URL

All examples use:

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

***

## Authentication

These endpoints support **two authentication methods**:

### 1) API Key (recommended for integrations)

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

```bash theme={null}
curl -X GET "https://www.niftipay.com/api/fiat/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.

***

# Concepts

## Amounts are stored in minor units

Fiat orders store monetary values as **minor units** (e.g. cents):

* `amountCents` — what the customer will pay (total)
* `subtotalCents` — the base subtotal (before service fee if customer pays)
* `serviceFeeCents` — total service maintenance fee (provider + platform), if applicable

### Currency minor unit rules

This implementation supports currencies with:

* 0 decimals (e.g. JPY)
* 2 decimals (most currencies)
* 3 decimals (e.g. BHD)

When you send `amount` as a string/number, it is converted to minor units using the currency’s decimal rules and rejects too many decimals.

***

## Service fee payer

The fiat checkout supports a **service maintenance fee** with two payer modes:

* `serviceFeePayer = "customer"`\
  Customer pays: **total = subtotal + fee**\
  The fee is shown as a line item in pricing.

* `serviceFeePayer = "merchant"`\
  Customer pays: **total = subtotal**\
  The fee is **deducted from merchant earnings** (not shown to customer as a separate line in pricing).

If `serviceFeePayer` is not provided on create, the system falls back to the user’s default (`GET /api/fiat/settings`).

***

## References and uniqueness

On creation, the route normalizes a reference from:

* `reference` (preferred)
* else `merchantReference` (fallback)

Then it checks **cross-type uniqueness** for your account:

* cannot conflict with a crypto order reference
* cannot conflict with an existing fiat order reference

If there is a conflict, the API returns `409`.

***

## Pricing and fee snapshots

Responses include:

### `pricing`

A stable, UI-friendly breakdown:

* `payer` (`customer` or `merchant`)
* `subtotalCents`, `serviceFeeCents`, `totalCents`
* `serviceFeePercent` (may be null)
* `lines` (Subtotal / Service maintenance fee / Total)

### `fees` (order details endpoint only)

A stable snapshot structure (written by `registerFiatOrderFees()`), including:

* service fee split (provider vs platform)
* payout fee / retention hold
* vendor net / payable now after deductions

Older orders may have some snapshot fields null; the API still returns a stable object with safe defaults.

***

## Payment URL + QR Code

All fiat order responses include:

* `payUrl` — the masked payment link to redirect the customer to
* `qrUrl` — a QR code image URL encoding the `payUrl`, ready to embed in checkout UIs

The `qrUrl` uses the same external QR API as crypto orders:

```
https://api.qrserver.com/v1/create-qr-code?size=300x300&data=<encoded payUrl>
```

You can display the QR image directly in an `<img>` tag or embed it in mobile checkout flows.

***

## Supported fiat currencies

The following fiat currencies are supported for fiat card orders:

| Code  | Currency                | Code  | Currency                  |
| ----- | ----------------------- | ----- | ------------------------- |
| `AED` | UAE Dirham              | `ISK` | Icelandic Krona           |
| `AMD` | Armenian Dram           | `JPY` | Japanese Yen              |
| `AUD` | Australian Dollar       | `KES` | Kenyan Shilling           |
| `BAM` | Bosnia-Herzegovina Mark | `KRW` | South Korean Won          |
| `BGN` | Bulgarian Lev           | `MDL` | Moldovan Leu              |
| `BRL` | Brazilian Real          | `MKD` | Macedonian Denar          |
| `BYN` | Belarusian Ruble        | `MWK` | Malawian Kwacha           |
| `CAD` | Canadian Dollar         | `MXN` | Mexican Peso              |
| `CHF` | Swiss Franc             | `MYR` | Malaysian Ringgit         |
| `CLP` | Chilean Peso            | `MZN` | Mozambican Metical        |
| `CNY` | Chinese Yuan            | `NGN` | Nigerian Naira            |
| `COP` | Colombian Peso          | `NOK` | Norwegian Krone           |
| `CRC` | Costa Rican Colon       | `PAB` | Panamanian Balboa         |
| `CZK` | Czech Koruna            | `PEN` | Peruvian Sol              |
| `DKK` | Danish Krone            | `PHP` | Philippine Peso           |
| `DOP` | Dominican Peso          | `PLN` | Polish Zloty              |
| `EUR` | Euro                    | `PYG` | Paraguayan Guarani        |
| `GBP` | British Pound           | `RON` | Romanian Leu              |
| `GEL` | Georgian Lari           | `RSD` | Serbian Dinar             |
| `GHS` | Ghanaian Cedi           | `RUB` | Russian Ruble             |
| `GIP` | Gibraltar Pound         | `SEK` | Swedish Krona             |
| `GTQ` | Guatemalan Quetzal      | `SGD` | Singapore Dollar          |
| `HUF` | Hungarian Forint        | `THB` | Thai Baht                 |
| `IDR` | Indonesian Rupiah       | `TRY` | Turkish Lira              |
|       |                         | `TWD` | New Taiwan Dollar         |
|       |                         | `TZS` | Tanzanian Shilling        |
|       |                         | `UAH` | Ukrainian Hryvnia         |
|       |                         | `UGX` | Ugandan Shilling          |
|       |                         | `USD` | US Dollar                 |
|       |                         | `UYU` | Uruguayan Peso            |
|       |                         | `VND` | Vietnamese Dong           |
|       |                         | `XAF` | Central African CFA Franc |
|       |                         | `XOF` | West African CFA Franc    |
|       |                         | `ZAR` | South African Rand        |
|       |                         | `ZMW` | Zambian Kwacha            |

> Currencies with 0 decimal places (e.g. `JPY`, `KRW`, `VND`, `XAF`, `XOF`) must be sent as whole numbers. Most currencies use 2 decimal places.

***

# List Fiat Orders

## Endpoint

`GET /api/fiat/orders`

Lists your fiat orders, newest first, using cursor pagination.

### Query parameters

| Name     | Type   | Default | Notes                                                               |
| -------- | ------ | ------- | ------------------------------------------------------------------- |
| `status` | string | —       | Optional filter.                                                    |
| `limit`  | number | 20      | Min 1, max 100.                                                     |
| `cursor` | number | —       | Use `nextCursor` from the previous response to fetch older records. |

### Example: list latest orders

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

### Example response

```json theme={null}
{
  "orders": [
    {
      "id": "6a4a2be6-6c6c-4a4a-bf9f-4f5b3e6a90aa",
      "orderKey": 2221,
      "userId": "user-id",
      "integrationId": "integration-id",
      "kind": "order",
      "amountCents": 7004,
      "subtotalCents": 6800,
      "serviceFeePayer": "customer",
      "serviceFeePercent": 3.0,
      "serviceFeeCents": 204,
      "currency": "GBP",
      "status": "new",
      "psp": "nopayn",
      "pspOrderId": "np_123",
      "pspProjectId": "proj_abc",
      "pspStatus": "new",
      "pspPaymentLinkId": null,
      "pspPaymentUrl": null,
      "orderUrl": "https://pay.nopayn.example/order/np_123",
      "returnUrl": "https://merchant.example/return",
      "failureUrl": "https://merchant.example/failure",
      "webhookUrl": "https://merchant.example/webhook",
      "merchantReference": "POS-1234",
      "email": null,
      "createdAt": "2026-02-11T10:00:00.000Z",
      "updatedAt": "2026-02-11T10:00:05.000Z",
      "completedAt": null,
      "pricing": {
        "payer": "customer",
        "subtotalCents": 6800,
        "serviceFeeCents": 204,
        "totalCents": 7004,
        "serviceFeePercent": 3.0,
        "lines": [
          { "type": "subtotal", "amountCents": 6800, "label": "Subtotal" },
          { "type": "service_maintenance_fee", "amountCents": 204, "label": "Service maintenance fee" },
          { "type": "total", "amountCents": 7004, "label": "Total" }
        ]
      },
      "qrUrl": "https://api.qrserver.com/v1/create-qr-code?size=300x300&data=https%3A%2F%2Fwww.niftipay.com%2Fpaylink%2F6a4a2be6-6c6c-4a4a-bf9f-4f5b3e6a90aa"
    }
  ],
  "nextCursor": 2221
}
```

### Pagination example

If `nextCursor` is not null, pass it back as `cursor` to fetch older results:

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

***

# Create Fiat Order

## Endpoint

`POST /api/fiat/orders`

Creates a fiat order and attempts to create the upstream NoPayn order. On success, returns:

* `order` (DB row)
* `pricing` (decorated)
* `display` (decimal strings for UI)
* `payUrl` (where to redirect the customer)
* `qrUrl` (QR code image encoding the `payUrl`)
* `nopayn` info

### Payment method gating

This endpoint enforces fiat availability for your account. If fiat card payments are disabled, it returns `403`.

### Request body

```json theme={null}
{
  "integrationId": "YOUR_FIAT_INTEGRATION_ID",
  "amount": "19.95",
  "currency": "EUR",
  "description": "Order #1001",
  "reference": "POS-1001",
  "serviceFeePayer": "customer",
  "email": "customer@example.com"
}
```

### Fields

| Field               | Type          | Required | Notes                                                                            |
| ------------------- | ------------- | -------- | -------------------------------------------------------------------------------- |
| `integrationId`     | string        | ✅        | Must exist and belong to you.                                                    |
| `currency`          | string        | ✅        | ISO currency code. See [supported currencies](#supported-fiat-currencies) below. |
| `amount`            | string/number | ⚠️       | Required unless `amountCents` is provided.                                       |
| `amountCents`       | number        | ⚠️       | Alternative to `amount`. Must be a positive integer.                             |
| `description`       | string        | ❌        | Passed to PSP for display.                                                       |
| `reference`         | string        | ❌        | Preferred merchant reference (normalized).                                       |
| `merchantReference` | string        | ❌        | Legacy alias; used if `reference` is missing.                                    |
| `serviceFeePayer`   | string        | ❌        | `"customer"` or `"merchant"`. Defaults to your fiat settings.                    |
| `email`             | string        | ❌        | Optional customer email. Stored with the order and returned in responses.        |

> Provide **either** `amount` **or** `amountCents`.

***

## Example: create order with amount string

```bash theme={null}
curl -X POST "https://www.niftipay.com/api/fiat/orders" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "integrationId": "integration-id",
    "amount": "68.00",
    "currency": "GBP",
    "description": "POS checkout",
    "reference": "POS-1234",
    "serviceFeePayer": "customer",
    "email": "customer@example.com"
  }'
```

### Example success response (201)

```json theme={null}
{
  "order": {
    "id": "6a4a2be6-6c6c-4a4a-bf9f-4f5b3e6a90aa",
    "orderKey": 2221,
    "userId": "user-id",
    "integrationId": "integration-id",
    "kind": "order",
    "amountCents": 7004,
    "subtotalCents": 6800,
    "serviceFeePayer": "customer",
    "serviceFeePercent": 3.0,
    "serviceFeeCents": 204,
    "currency": "GBP",
    "status": "new",
    "psp": "nopayn",
    "pspProjectId": "proj_abc",
    "pspOrderId": "np_123",
    "pspStatus": "new",
    "orderUrl": "https://pay.nopayn.example/order/np_123",
    "returnUrl": "https://merchant.example/return",
    "failureUrl": "https://merchant.example/failure",
    "webhookUrl": "https://merchant.example/webhook",
    "merchantReference": "POS-1234",
    "email": "customer@example.com",
    "createdAt": "2026-02-11T10:00:00.000Z",
    "updatedAt": "2026-02-11T10:00:05.000Z",
    "completedAt": null
  },
  "pricing": {
    "payer": "customer",
    "subtotalCents": 6800,
    "serviceFeeCents": 204,
    "totalCents": 7004,
    "serviceFeePercent": 3.0,
    "lines": [
      { "type": "subtotal", "amountCents": 6800, "label": "Subtotal" },
      { "type": "service_maintenance_fee", "amountCents": 204, "label": "Service maintenance fee" },
      { "type": "total", "amountCents": 7004, "label": "Total" }
    ],
    "serviceFeeBreakdown": {
      "providerPercent": 2.0,
      "platformPercent": 1.0,
      "totalPercent": 3.0,
      "providerCents": 136,
      "platformCents": 68,
      "totalCents": 204
    }
  },
  "display": {
    "subtotal": "68.00",
    "serviceFee": "2.04",
    "total": "70.04",
    "currency": "GBP"
  },
  "reference": "POS-1234",
  "nopayn": {
    "id": "np_123",
    "status": "new",
    "order_url": "https://pay.nopayn.example/order/np_123"
  },
  "payUrl": "https://pay.nopayn.example/order/np_123",
  "qrUrl": "https://api.qrserver.com/v1/create-qr-code?size=300x300&data=https%3A%2F%2Fwww.niftipay.com%2Fpaylink%2F6a4a2be6-6c6c-4a4a-bf9f-4f5b3e6a90aa"
}
```

> `payUrl` is what you should open/redirect the customer to. `qrUrl` is a ready-to-use QR code image that encodes the same payment link.

***

## Example: create order with amountCents

```bash theme={null}
curl -X POST "https://www.niftipay.com/api/fiat/orders" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "integrationId": "integration-id",
    "amountCents": 1995,
    "currency": "EUR",
    "description": "Order #1001",
    "reference": "POS-1001"
  }'
```

***

## Common create errors

### Invalid JSON (400)

```json theme={null}
{ "error": "Invalid JSON" }
```

### Payment method disabled (403)

```json theme={null}
{ "error": "Payment method disabled" }
```

### Missing required fields (400)

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

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

### Invalid amount (400)

```json theme={null}
{ "error": "amount is required (e.g. 19.95)" }
```

```json theme={null}
{ "error": "amountCents must be a positive integer" }
```

```json theme={null}
{ "error": "amount has too many decimals for JPY" }
```

### Invalid serviceFeePayer (400)

```json theme={null}
{ "error": "serviceFeePayer must be \"customer\" or \"merchant\"" }
```

### Reference conflict (409)

```json theme={null}
{ "error": "Reference used by crypto order" }
```

or

```json theme={null}
{ "error": "Reference already exists" }
```

### Integration not found (404)

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

### Upstream PSP error (502)

If NoPayn fails, the order is marked `status="error"` locally and you get:

```json theme={null}
{ "error": "Upstream error", "details": null }
```

***

# Delete Fiat Order (by reference)

## Endpoint

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

This is a **proxy-friendly** delete that mirrors your crypto delete behavior.

Matching rules:

1. first tries strict match: `fiatOrder.merchantReference === reference` (newest first)
2. fallback: if reference is numeric, match by `fiatOrder.orderKey === Number(reference)`

### Safety rules

Deletion is conservative:

* You **cannot** delete `completed`, `paid`, or `refunded` orders
* If `pspOrderId` exists, the order must already be `cancelled` before deletion

### Example request

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

### Success response

* `204 No Content`

### Error responses

**Missing reference (400)**

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

**Not found (404)**

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

**Cannot delete completed/paid/refunded (409)**

```json theme={null}
{ "error": "Cannot delete – order status is \"completed\"" }
```

**Must cancel first (409)**

```json theme={null}
{
  "error": "Cannot delete – order must be cancelled first (call DELETE /api/fiat/orders/cancel?reference=...)"
}
```

***

# Get Fiat Order (by orderKey)

## Endpoint

`GET /api/fiat/orders/:orderKey`

Returns:

* `order` (DB row)
* `pricing` (decorated)
* `fees` (snapshot breakdown)
* `payUrl` (masked payment link)
* `qrUrl` (QR code image for the payment link)

### Example request

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

### Example response

```json theme={null}
{
  "order": {
    "id": "6a4a2be6-6c6c-4a4a-bf9f-4f5b3e6a90aa",
    "orderKey": 2221,
    "userId": "user-id",
    "integrationId": "integration-id",
    "amountCents": 7004,
    "subtotalCents": 6800,
    "serviceFeePayer": "customer",
    "serviceFeePercent": 3.0,
    "serviceFeeCents": 204,
    "currency": "GBP",
    "status": "new",
    "psp": "nopayn",
    "pspOrderId": "np_123",
    "pspProjectId": "proj_abc",
    "pspStatus": "new",
    "orderUrl": "https://pay.nopayn.example/order/np_123",
    "returnUrl": "https://merchant.example/return",
    "failureUrl": "https://merchant.example/failure",
    "webhookUrl": "https://merchant.example/webhook",
    "merchantReference": "POS-1234",
    "email": "customer@example.com",
    "createdAt": "2026-02-11T10:00:00.000Z",
    "updatedAt": "2026-02-11T10:00:05.000Z",
    "completedAt": null,

    "serviceFeeProviderPercent": 2.0,
    "serviceFeePlatformPercent": 1.0,
    "serviceFeeProviderCents": 136,
    "serviceFeePlatformCents": 68,
    "payoutFeePercent": 0,
    "payoutFeeCents": 0,
    "retentionPercent": 10,
    "retentionHoldCents": 680,
    "payableNowCents": 6120,
    "feeSnapshotAt": "2026-02-11T10:00:05.000Z",
    "feeSnapshotVersion": 1
  },
  "pricing": {
    "payer": "customer",
    "subtotalCents": 6800,
    "serviceFeeCents": 204,
    "totalCents": 7004,
    "serviceFeePercent": 3.0,
    "lines": [
      { "type": "subtotal", "amountCents": 6800, "label": "Subtotal" },
      { "type": "service_maintenance_fee", "amountCents": 204, "label": "Service maintenance fee" },
      { "type": "total", "amountCents": 7004, "label": "Total" }
    ]
  },
  "fees": {
    "snapshotAt": "2026-02-11T10:00:05.000Z",
    "snapshotVersion": 1,
    "serviceFee": {
      "payer": "customer",
      "totalCents": 204,
      "providerCents": 136,
      "platformCents": 68,
      "providerPercent": 2.0,
      "platformPercent": 1.0
    },
    "payout": {
      "payoutFeePercent": 0,
      "payoutFeeCents": 0,
      "retentionPercent": 10,
      "retentionHoldCents": 680,
      "payableNowCents": 6120
    },
    "vendor": {
      "netAfterFeesCents": 6120,
      "payableNowAfterServiceFeeCents": 6120,
      "serviceFeeMerchantDeductionCents": 0
    }
  },
  "payUrl": "https://www.niftipay.com/paylink/6a4a2be6-6c6c-4a4a-bf9f-4f5b3e6a90aa",
  "qrUrl": "https://api.qrserver.com/v1/create-qr-code?size=300x300&data=https%3A%2F%2Fwww.niftipay.com%2Fpaylink%2F6a4a2be6-6c6c-4a4a-bf9f-4f5b3e6a90aa",
  "matchedBy": "orderKey"
}
```

***

# Cancel Fiat Order (by orderKey)

## Endpoint

`DELETE /api/fiat/orders/:orderKey`

This attempts to cancel the upstream PSP order (NoPayn) and then updates the local DB.

### Behavior

* If already cancelled, returns current state (idempotent).
* If status is `completed`, cancellation is blocked (safe default).
* Requires `pspOrderId` to exist.
* Only `psp="nopayn"` is supported here.

### Example request

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

### Example response

```json theme={null}
{
  "order": {
    "orderKey": 2221,
    "status": "cancelled",
    "pspOrderId": "np_123",
    "pspStatus": "cancelled",
    "orderUrl": "https://pay.nopayn.example/order/np_123"
  },
  "pricing": {
    "payer": "customer",
    "subtotalCents": 6800,
    "serviceFeeCents": 204,
    "totalCents": 7004,
    "serviceFeePercent": 3.0,
    "lines": [
      { "type": "subtotal", "amountCents": 6800, "label": "Subtotal" },
      { "type": "service_maintenance_fee", "amountCents": 204, "label": "Service maintenance fee" },
      { "type": "total", "amountCents": 7004, "label": "Total" }
    ]
  },
  "fees": {
    "snapshotAt": "2026-02-11T10:00:05.000Z",
    "snapshotVersion": 1,
    "serviceFee": { "payer": "customer", "totalCents": 204, "providerCents": 136, "platformCents": 68, "providerPercent": 2.0, "platformPercent": 1.0 },
    "payout": { "payoutFeePercent": 0, "payoutFeeCents": 0, "retentionPercent": 10, "retentionHoldCents": 680, "payableNowCents": 6120 },
    "vendor": { "netAfterFeesCents": 6120, "payableNowAfterServiceFeeCents": 6120, "serviceFeeMerchantDeductionCents": 0 }
  },
  "nopayn": {
    "id": "np_123",
    "status": "cancelled",
    "order_url": "https://pay.nopayn.example/order/np_123"
  }
}
```

### Cancel error examples

**Invalid orderKey (400)**

```json theme={null}
{ "error": "Invalid orderKey" }
```

**Not found (404)**

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

**Completed cannot be cancelled (409)**

```json theme={null}
{ "error": "Order is completed and cannot be cancelled" }
```

**Missing pspOrderId (409)**

```json theme={null}
{ "error": "Missing PSP order id (pspOrderId)" }
```

**PSP not supported (501)**

```json theme={null}
{ "error": "Cancel not implemented for psp=stripe" }
```

**Upstream cancel failed (502 or provider status code)**

```json theme={null}
{ "error": "Upstream cancel failed", "details": null }
```

***

# Cancel Fiat Order (by reference)

## Endpoint

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

Cancel using the same matching rules as delete-by-reference:

1. strict merchantReference match
2. numeric fallback to orderKey

### Example request

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

### Responses

* Returns `order` + `pricing` + `nopayn` on success
* Same error patterns as cancel by orderKey

***

# Refunds (Fiat)

Refunds are executed at the PSP level (NoPayn) and support **partial / multiple refunds**.

Refund creation is conservative:

* Only for `kind="order"` (not payment links)
* Only allowed when order is `paid` or `completed`
* Prevents over-refund by reading current refunds from NoPayn first

Refund ceiling depends on service fee payer:

* If `serviceFeePayer="customer"`: refundable ceiling = **total paid** (`amountCents`)
* If `serviceFeePayer="merchant"`: refundable ceiling = **subtotal only** (`subtotalCents`)\
  (Because the customer never paid the service fee)

***

## List refunds

### Endpoint

`GET /api/fiat/orders/:orderKey/refunds`

Returns:

* `refunds` (from NoPayn)
* computed meta: `maxRefundableCents`, `refundedCents`, `remainingRefundableCents`

### Example request

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

### Example response

```json theme={null}
{
  "order": {
    "orderKey": 2221,
    "amountCents": 7004,
    "subtotalCents": 6800,
    "serviceFeePayer": "customer",
    "currency": "GBP",
    "status": "completed",
    "pspOrderId": "np_123"
  },
  "nopaynOrderId": "np_123",
  "refunds": [
    { "id": "rf_1", "amount": 1000, "status": "completed", "created": "2026-02-11T12:00:00.000Z" }
  ],
  "meta": {
    "serviceFeePayer": "customer",
    "maxRefundableCents": 7004,
    "refundedCents": 1000,
    "remainingRefundableCents": 6004
  }
}
```

### Common list-refunds errors

**Invalid orderKey (400)**

```json theme={null}
{ "error": "Invalid orderKey" }
```

**Not found (404)**

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

**Missing pspOrderId (400)**

```json theme={null}
{
  "error": "Order is missing pspOrderId (NoPayn order id). It may not have been created in NoPayn yet."
}
```

**NoPayn upstream error (502)**

```json theme={null}
{ "error": "Failed to fetch refunds from NoPayn", "details": null }
```

***

## Create refund

### Endpoint

`POST /api/fiat/orders/:orderKey/refunds`

### Request body

```json theme={null}
{
  "amountCents": 1000,
  "description": "Partial refund"
}
```

Optional: specify order lines (for item-based refunds):

```json theme={null}
{
  "amountCents": 1000,
  "description": "Refund 1 item",
  "orderLines": [
    { "merchantOrderLineId": "line-1", "quantity": 1 }
  ]
}
```

### Fields

| Field         | Type       | Required | Notes                                                                                       |
| ------------- | ---------- | -------- | ------------------------------------------------------------------------------------------- |
| `amountCents` | number     | ✅        | Positive integer. Must not exceed remaining refundable.                                     |
| `description` | string     | ❌        | Optional PSP description.                                                                   |
| `orderLines`  | array/null | ❌        | If provided: must be a non-empty array with `{ merchantOrderLineId, quantity }`, or `null`. |

> `orderLines` validation:
>
> * `orderLines` must be **non-empty** if provided as an array
> * each line requires a non-empty `merchantOrderLineId`
> * `quantity` must be a positive integer

### Example request

```bash theme={null}
curl -X POST "https://www.niftipay.com/api/fiat/orders/2221/refunds" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "amountCents": 1000,
    "description": "Partial refund"
  }'
```

### Example response (201)

```json theme={null}
{
  "order": {
    "id": "6a4a2be6-6c6c-4a4a-bf9f-4f5b3e6a90aa",
    "orderKey": 2221,
    "status": "completed",
    "pspOrderId": "np_123",
    "currency": "GBP",
    "amountCents": 7004,
    "subtotalCents": 6800,
    "serviceFeePayer": "customer"
  },
  "nopaynOrderId": "np_123",
  "refundOrder": {
    "id": "rf_2",
    "amount": 1000,
    "status": "completed"
  },
  "refunds": [
    { "id": "rf_1", "amount": 1000, "status": "completed" },
    { "id": "rf_2", "amount": 1000, "status": "completed" }
  ],
  "meta": {
    "serviceFeePayer": "customer",
    "maxRefundableCents": 7004,
    "refundedCents": 2000,
    "remainingRefundableCents": 5004
  }
}
```

If the order becomes fully refunded (remaining refundable reaches 0), the local `fiatOrder.status` is updated to `"refunded"`.

***

## Common refund errors

**Invalid JSON (400)**

```json theme={null}
{ "error": "Invalid JSON" }
```

**Refunds only for paid/completed (409)**

```json theme={null}
{ "error": "Refunds are only allowed for paid/completed orders (current status: \"new\")" }
```

**Refunds only for kind=order (400)**

```json theme={null}
{ "error": "Refunds can only be created for kind=order (not payment_link)." }
```

**Missing pspOrderId (400)**

```json theme={null}
{
  "error": "Order is missing pspOrderId (NoPayn order id). It may not have been created in NoPayn yet."
}
```

**Nothing left to refund (409)**

```json theme={null}
{ "error": "Nothing left to refund for this order." }
```

**Amount exceeds remaining refundable (400)**

```json theme={null}
{
  "error": "amountCents cannot exceed remaining refundable (6004)",
  "meta": { "maxRefundableCents": 7004, "refundedCents": 1000, "remainingRefundableCents": 6004 }
}
```

**Invalid orderLines (400)**

```json theme={null}
{ "error": "orderLines cannot be an empty array" }
```

```json theme={null}
{ "error": "orderLines[].merchantOrderLineId is required" }
```

```json theme={null}
{ "error": "orderLines[].quantity must be a positive integer" }
```

**NoPayn refund create failed (502)**

```json theme={null}
{ "error": "Failed to create refund in NoPayn", "details": null }
```

***

## Node.js Example

A complete Node.js example showing how to create a fiat payment order with pricing breakdown and response parsing.

```javascript theme={null}
const result = await apiRequest("POST", "/api/fiat/orders", {
  // Your fiat integration ID (Dashboard → Settings → Fiat)
  integrationId: "YOUR_INTEGRATION_ID",

  // Fiat currency (ISO 4217 code)
  currency: "EUR",

  // Amount in major units (e.g. 19.95 = €19.95)
  amount: 19.95,
  // Or use minor units: amountCents: 1995

  // Short description shown on payment page (optional)
  description: "Widget purchase",

  // Your unique reference (optional — max 80 characters)
  reference: "FIAT-ORDER-001",

  // Who pays the processing fee: "customer" or "merchant" (optional)
  serviceFeePayer: "customer",

  // Customer email for receipts (optional)
  email: "jane.doe@example.com",
});

// Key response fields:
// result.order.payUrl              — redirect customer here
// result.order.id                  — Niftipay order ID
// result.reference                 — your reference
// result.order.status              — "new" initially
// result.qrUrl                     — QR code for payment page
// result.display.subtotal          — requested amount
// result.display.serviceFee        — processing fee
// result.display.total             — total charged
// result.pricing.payer             — who pays the fee
// result.pricing.serviceFeeBreakdown.totalPercent — fee %
```

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

***

# Status notes

Fiat order status values come from the PSP status field in practice (e.g. NoPayn’s `status`), plus local states:

* `new` — created locally (and usually also created upstream)
* `cancelled` — cancelled locally (and upstream when possible)
* `completed` / `paid` — successful payment
* `refunded` — fully refunded (local state set when refundable remaining reaches 0)
* `error` — upstream create failed

***
