# Invoices

Base URL: `https://txnod.com/api/v1`. Every request must carry the three HMAC headers:

- `X-Project-Id` — the caller's project ULID.
- `X-Timestamp` — current Unix timestamp (seconds). Requests outside the ±300-second window are rejected with `timestamp_out_of_window`.
- `X-Signature` — HMAC-SHA256 hex digest of `${timestamp}.${rawBody}` using the project's API secret.

The SDK (`@txnod/sdk`) handles signing transparently — prefer it over hand-rolled cURL except for smoke tests.

## Create an invoice

`POST https://txnod.com/api/v1/invoices` — creates a new invoice. Idempotent on `(project_id, external_id)`: a replayed request returns the existing invoice with HTTP 200 instead of 201.

### Request

{/* @schema:createInvoice:request */}

**Request body** (`application/json`)

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `external_id` | string | yes | min length: 1; max length: 128 |
| `amount_usd` | number | no | exclusive min: 0 |
| `amount_crypto` | string | no | pattern: `^\d+(\.\d+)?$` |
| `coin` | `"btc"` \| `"eth"` \| `"usdt_erc20"` \| `"usdc_erc20"` \| `"trx"` \| `"usdt_trc20"` \| `"ada"` \| `"pol"` \| `"usdt_polygon"` \| `"usdc_polygon"` \| `"bnb"` \| `"usdt_bep20"` \| `"usdc_bep20"` \| `"ton"` \| `"usdt_ton"` | yes | — |
| `callback_url` | string (URI) | no | — |
| `metadata` | object | no | — |

{/* @end:schema:createInvoice:request */}

### Response

{/* @schema:createInvoice:response */}

**200** — Idempotent replay — existing invoice returned unchanged

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (ULID) | yes | — |
| `project_id` | string (ULID) | yes | — |
| `external_id` | string | yes | — |
| `coin` | `"btc"` \| `"eth"` \| `"usdt_erc20"` \| `"usdc_erc20"` \| `"trx"` \| `"usdt_trc20"` \| `"ada"` \| `"pol"` \| `"usdt_polygon"` \| `"usdc_polygon"` \| `"bnb"` \| `"usdt_bep20"` \| `"usdc_bep20"` \| `"ton"` \| `"usdt_ton"` | yes | — |
| `address` | string | yes | — |
| `amount_crypto` | string | yes | — |
| `amount_crypto_units` | string | yes | — |
| `amount_usd` | number \| null | yes | — |
| `rate_snapshot` | object \| null | yes | — |
| `payment_token` | string \| null | yes | pattern: `^[0-9a-f]{8}$` |
| `payment_uri` | string | yes | — |
| `callback_url` | string \| null | yes | — |
| `metadata` | object \| null | yes | — |
| `matching_mode` | `"exact"` \| `"at_least"` \| `"any"` | yes | — |
| `confirmation_threshold` | integer | yes | min: 0 |
| `status` | `"pending"` \| `"detected"` \| `"paid"` \| `"overpaid"` \| `"partial"` \| `"expired"` \| `"expired_paid_late"` \| `"reverted"` \| `"cancelled"` | yes | — |
| `expires_at` | integer | yes | min: 0 |
| `expires_at_iso` | string (ISO 8601) | yes | — |
| `created_at` | integer | yes | min: 0 |
| `created_at_iso` | string (ISO 8601) | yes | — |
| `derivation_path` | string | no | pattern: `^m(\/\d+'?)+$` |
| `verification_standard` | `"bip84"` \| `"bip44"` \| `"cip1852"` \| `"bip44_ed25519"` | no | — |
| `transactions` | array&lt;object&gt; | no | — |
| `confirmations` | integer | no | min: 0 |

**201** — Invoice created

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (ULID) | yes | — |
| `project_id` | string (ULID) | yes | — |
| `external_id` | string | yes | — |
| `coin` | `"btc"` \| `"eth"` \| `"usdt_erc20"` \| `"usdc_erc20"` \| `"trx"` \| `"usdt_trc20"` \| `"ada"` \| `"pol"` \| `"usdt_polygon"` \| `"usdc_polygon"` \| `"bnb"` \| `"usdt_bep20"` \| `"usdc_bep20"` \| `"ton"` \| `"usdt_ton"` | yes | — |
| `address` | string | yes | — |
| `amount_crypto` | string | yes | — |
| `amount_crypto_units` | string | yes | — |
| `amount_usd` | number \| null | yes | — |
| `rate_snapshot` | object \| null | yes | — |
| `payment_token` | string \| null | yes | pattern: `^[0-9a-f]{8}$` |
| `payment_uri` | string | yes | — |
| `callback_url` | string \| null | yes | — |
| `metadata` | object \| null | yes | — |
| `matching_mode` | `"exact"` \| `"at_least"` \| `"any"` | yes | — |
| `confirmation_threshold` | integer | yes | min: 0 |
| `status` | `"pending"` \| `"detected"` \| `"paid"` \| `"overpaid"` \| `"partial"` \| `"expired"` \| `"expired_paid_late"` \| `"reverted"` \| `"cancelled"` | yes | — |
| `expires_at` | integer | yes | min: 0 |
| `expires_at_iso` | string (ISO 8601) | yes | — |
| `created_at` | integer | yes | min: 0 |
| `created_at_iso` | string (ISO 8601) | yes | — |
| `derivation_path` | string | no | pattern: `^m(\/\d+'?)+$` |
| `verification_standard` | `"bip84"` \| `"bip44"` \| `"cip1852"` \| `"bip44_ed25519"` | no | — |
| `transactions` | array&lt;object&gt; | no | — |
| `confirmations` | integer | no | min: 0 |

{/* @end:schema:createInvoice:response */}

### Errors


| Status | Error code(s) | Trigger |
| --- | --- | --- |
| 400 | `validation_error`, `invalid_coin`, `invalid_xpub_format`, `invalid_webhook_url` | Request body or query failed validation |
| 401 | `auth_invalid`, `signature_invalid`, `timestamp_out_of_window` | HMAC authentication failed |
| 402 | `subscription_expired` | Operator's TxNod subscription is not `active`; writes are blocked. Operator must renew via dashboard `/billing` |
| 403 | `key_revoked`, `key_suspended`, `project_suspended`, `permission_denied` | Auth key or project disabled by operator action |
| 409 | `external_id_conflict`, `xpub_not_verified` | Idempotent replay or ownership challenge incomplete |
| 422 | `coin_not_enabled`, `amount_out_of_range`, `wallet_not_bound`, `tron_no_activated_addresses_available` | Coin not enabled, amount out of range, no verified wallet bound for the requested chain on the project, or — TRON only — operator's address pool has zero activated rows |
| 429 | `rate_limit_exceeded` | Per-project invoice-create rate limit exceeded |
| 500 | `internal_error` | Internal error (including cold-start rate unavailability) |
| 503 | `pool_exhausted` | Address pool at hard cap; retry after `Retry-After` |


### Examples

```bash
curl -X POST https://txnod.com/api/v1/invoices \
  -H "X-Project-Id: $TXNOD_PROJECT_ID" \
  -H "X-Timestamp: $(date +%s)" \
  -H "X-Signature: <hex>" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"order-123","amount_usd":10.0,"coin":"btc"}'
```

```ts
import { TxnodClient } from '@txnod/sdk';

const client = new TxnodClient({
  projectId: process.env.TXNOD_PROJECT_ID!,
  apiSecret: process.env.TXNOD_API_SECRET!,
});

const invoice = await client.createInvoice({
  external_id: 'order-123',
  amount_usd: 10.0,
  coin: 'btc',
});
```

## Search invoices

`GET https://txnod.com/api/v1/invoices` — cursor-paginated search over the caller project's invoices.

### Request

{/* @schema:searchInvoices:request */}

**Query parameters**

| Param | Type | Required | Constraints |
| --- | --- | --- | --- |
| `external_id` | string | no | — |
| `address` | string | no | — |
| `tx_hash` | string | no | — |
| `amount` | string | no | — |
| `status` | `"pending"` \| `"detected"` \| `"paid"` \| `"overpaid"` \| `"partial"` \| `"expired"` \| `"expired_paid_late"` \| `"reverted"` \| `"cancelled"` | no | — |
| `date_from` | string (ISO 8601) | no | — |
| `date_to` | string (ISO 8601) | no | — |
| `cursor` | string (ULID) | no | — |
| `limit` | integer | no | min: 1; max: 200; default: `50` |

{/* @end:schema:searchInvoices:request */}

### Response

{/* @schema:searchInvoices:response */}

**200** — Cursor-paginated invoice list

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `items` | array&lt;object&gt; | yes | — |
| `next_cursor` | string (ULID) | no | — |

{/* @end:schema:searchInvoices:response */}

### Errors


| Status | Error code(s) | Trigger |
| --- | --- | --- |
| 400 | `validation_error` | Query validation error |
| 401 | `auth_invalid`, `signature_invalid`, `timestamp_out_of_window` | HMAC authentication failed |


### Examples

```bash
curl "https://txnod.com/api/v1/invoices?status=paid&limit=20" \
  -H "X-Project-Id: $TXNOD_PROJECT_ID" \
  -H "X-Timestamp: $(date +%s)" \
  -H "X-Signature: <hex>"
```

```ts
import { TxnodClient } from '@txnod/sdk';

const client = new TxnodClient({
  projectId: process.env.TXNOD_PROJECT_ID!,
  apiSecret: process.env.TXNOD_API_SECRET!,
});

const page = await client.searchInvoices({ status: 'paid', limit: 20 });
for (const invoice of page.items) console.log(invoice.id);
```

## Retrieve an invoice

`GET https://txnod.com/api/v1/invoices/{id}` — fetch a single invoice by ULID. Returns 404 on cross-project lookups (no enumeration).

### Request

{/* @schema:getInvoice:request */}

**Path parameters**

| Param | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (ULID) | yes | — |

{/* @end:schema:getInvoice:request */}

### Response

{/* @schema:getInvoice:response */}

**200** — Invoice detail including transactions + confirmations

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (ULID) | yes | — |
| `project_id` | string (ULID) | yes | — |
| `external_id` | string | yes | — |
| `coin` | `"btc"` \| `"eth"` \| `"usdt_erc20"` \| `"usdc_erc20"` \| `"trx"` \| `"usdt_trc20"` \| `"ada"` \| `"pol"` \| `"usdt_polygon"` \| `"usdc_polygon"` \| `"bnb"` \| `"usdt_bep20"` \| `"usdc_bep20"` \| `"ton"` \| `"usdt_ton"` | yes | — |
| `address` | string | yes | — |
| `amount_crypto` | string | yes | — |
| `amount_crypto_units` | string | yes | — |
| `amount_usd` | number \| null | yes | — |
| `rate_snapshot` | object \| null | yes | — |
| `payment_token` | string \| null | yes | pattern: `^[0-9a-f]{8}$` |
| `payment_uri` | string | yes | — |
| `callback_url` | string \| null | yes | — |
| `metadata` | object \| null | yes | — |
| `matching_mode` | `"exact"` \| `"at_least"` \| `"any"` | yes | — |
| `confirmation_threshold` | integer | yes | min: 0 |
| `status` | `"pending"` \| `"detected"` \| `"paid"` \| `"overpaid"` \| `"partial"` \| `"expired"` \| `"expired_paid_late"` \| `"reverted"` \| `"cancelled"` | yes | — |
| `expires_at` | integer | yes | min: 0 |
| `expires_at_iso` | string (ISO 8601) | yes | — |
| `created_at` | integer | yes | min: 0 |
| `created_at_iso` | string (ISO 8601) | yes | — |
| `derivation_path` | string | no | pattern: `^m(\/\d+'?)+$` |
| `verification_standard` | `"bip84"` \| `"bip44"` \| `"cip1852"` \| `"bip44_ed25519"` | no | — |
| `transactions` | array&lt;object&gt; | no | — |
| `confirmations` | integer | no | min: 0 |

{/* @end:schema:getInvoice:response */}

### Errors


| Status | Error code(s) | Trigger |
| --- | --- | --- |
| 401 | `auth_invalid`, `signature_invalid`, `timestamp_out_of_window` | HMAC authentication failed |
| 404 | `invoice_not_found` | Invoice not found or belongs to a different project |


### Examples

```bash
curl https://txnod.com/api/v1/invoices/01HK8MAR2QEXAMPLE000000000 \
  -H "X-Project-Id: $TXNOD_PROJECT_ID" \
  -H "X-Timestamp: $(date +%s)" \
  -H "X-Signature: <hex>"
```

```ts
import { TxnodClient } from '@txnod/sdk';

const client = new TxnodClient({
  projectId: process.env.TXNOD_PROJECT_ID!,
  apiSecret: process.env.TXNOD_API_SECRET!,
});

const invoice = await client.getInvoice('01HK8MAR2QEXAMPLE000000000');
console.log(invoice.status);
```

## Cancel an invoice

`POST https://txnod.com/api/v1/invoices/{id}/cancel` — cancels an invoice in `pending` or `detected` state and releases its pool address into cooldown. Terminal-status invoices return 409 `invoice_not_cancellable`.

### Request

{/* @schema:cancelInvoice:request */}

**Path parameters**

| Param | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (ULID) | yes | — |

{/* @end:schema:cancelInvoice:request */}

### Response

{/* @schema:cancelInvoice:response */}

**200** — Invoice cancelled

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (ULID) | yes | — |
| `project_id` | string (ULID) | yes | — |
| `external_id` | string | yes | — |
| `coin` | `"btc"` \| `"eth"` \| `"usdt_erc20"` \| `"usdc_erc20"` \| `"trx"` \| `"usdt_trc20"` \| `"ada"` \| `"pol"` \| `"usdt_polygon"` \| `"usdc_polygon"` \| `"bnb"` \| `"usdt_bep20"` \| `"usdc_bep20"` \| `"ton"` \| `"usdt_ton"` | yes | — |
| `address` | string | yes | — |
| `amount_crypto` | string | yes | — |
| `amount_crypto_units` | string | yes | — |
| `amount_usd` | number \| null | yes | — |
| `rate_snapshot` | object \| null | yes | — |
| `payment_token` | string \| null | yes | pattern: `^[0-9a-f]{8}$` |
| `payment_uri` | string | yes | — |
| `callback_url` | string \| null | yes | — |
| `metadata` | object \| null | yes | — |
| `matching_mode` | `"exact"` \| `"at_least"` \| `"any"` | yes | — |
| `confirmation_threshold` | integer | yes | min: 0 |
| `status` | `"pending"` \| `"detected"` \| `"paid"` \| `"overpaid"` \| `"partial"` \| `"expired"` \| `"expired_paid_late"` \| `"reverted"` \| `"cancelled"` | yes | — |
| `expires_at` | integer | yes | min: 0 |
| `expires_at_iso` | string (ISO 8601) | yes | — |
| `created_at` | integer | yes | min: 0 |
| `created_at_iso` | string (ISO 8601) | yes | — |
| `derivation_path` | string | no | pattern: `^m(\/\d+'?)+$` |
| `verification_standard` | `"bip84"` \| `"bip44"` \| `"cip1852"` \| `"bip44_ed25519"` | no | — |
| `transactions` | array&lt;object&gt; | no | — |
| `confirmations` | integer | no | min: 0 |

{/* @end:schema:cancelInvoice:response */}

### Errors


| Status | Error code(s) | Trigger |
| --- | --- | --- |
| 401 | `auth_invalid`, `signature_invalid`, `timestamp_out_of_window` | HMAC authentication failed |
| 404 | `invoice_not_found` | Invoice not found or belongs to a different project |
| 409 | `invoice_not_cancellable`, `invalid_state_transition` | Invoice is not in a cancellable state |


### Examples

```bash
curl -X POST https://txnod.com/api/v1/invoices/01HK8MAR2QEXAMPLE000000000/cancel \
  -H "X-Project-Id: $TXNOD_PROJECT_ID" \
  -H "X-Timestamp: $(date +%s)" \
  -H "X-Signature: <hex>"
```

```ts
import { TxnodClient } from '@txnod/sdk';

const client = new TxnodClient({
  projectId: process.env.TXNOD_PROJECT_ID!,
  apiSecret: process.env.TXNOD_API_SECRET!,
});

const cancelled = await client.cancelInvoice('01HK8MAR2QEXAMPLE000000000');
console.log(cancelled.status);
```