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

# Invoices

> Create draft invoices, compose lines, and collect payment automatically or via a hosted page.

Invoices let you record pending charges for a customer, compose them into a document with a gapless number, and collect payment off-session or by directing the buyer to a hosted page.

On Node, `npm install @ionicfi/sdk` gives you typed calls for every request
below; the [server SDK](/sdks/server) guide has details. Each example also
shows raw curl.

## Lifecycle

<Steps>
  <Step title="(Optional) Record pending charges">
    Create invoice items against a customer before invoicing. When you create an invoice for that customer, every pending item in the invoice currency drains onto the new draft automatically.
  </Step>

  <Step title="Create a draft invoice">
    Call `POST /v1/invoices` with a customer, currency, and collection method. The draft starts with all pending items already applied as lines. Drafts carry no number and do not appear in sequences.
  </Step>

  <Step title="Edit the draft">
    Add or remove individual lines before finalizing. Totals recompute on each change.
  </Step>

  <Step title="Finalize">
    Call `POST /v1/invoices/{id}/finalize` to assign a sequential number, apply tax, and freeze the lines. A zero-total invoice transitions directly to `paid`.
  </Step>

  <Step title="Collect payment">
    For `charge_automatically` invoices, call `POST /v1/invoices/{id}/pay`. For `send_invoice` invoices, Ionic provides a hosted invoice page where the buyer pays directly.
  </Step>
</Steps>

## Record pending charges

Invoice items record a charge against a customer without immediately billing them. When you create an invoice for that customer, all pending items in the invoice currency are drained onto it as lines and cleared from the pending queue.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { Ionic } from "@ionicfi/sdk";

  const ionic = new Ionic({ token: process.env.IONIC_SECRET_KEY });

  const item = await ionic.invoices.items.create({
    "Idempotency-Key": "ii_seats_feb_cus_abc",
    customer: "cus_000000000000000000000000",
    description: "5 additional seats — February",
    quantity: 5,
    unit_amount: 1000,
    currency: "usd",
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/invoice_items \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: ii_seats_feb_cus_abc" \
    -H "Content-Type: application/json" \
    -d '{
      "customer": "cus_000000000000000000000000",
      "description": "5 additional seats — February",
      "quantity": 5,
      "unit_amount": 1000,
      "currency": "usd"
    }'
  ```
</CodeGroup>

| Field         | Type    | Notes                                                                                        |
| ------------- | ------- | -------------------------------------------------------------------------------------------- |
| `customer`    | string  | Required. Customer id (`cus_…`).                                                             |
| `description` | string  | Required. Display label for this charge.                                                     |
| `quantity`    | integer | Required. Minimum 1.                                                                         |
| `unit_amount` | integer | Required. Per-unit price in minor units (e.g., cents for USD).                               |
| `currency`    | string  | Required. Lowercase ISO currency code. Must match the invoice currency when the item drains. |
| `price`       | string  | Optional price id (`price_…`) for catalog reference.                                         |
| `metadata`    | object  | Key-value data attached to this item.                                                        |

An invoice item stays pending until an invoice drains it. A pending item can be updated (`POST /v1/invoice_items/{id}`) or deleted (`DELETE /v1/invoice_items/{id}`). Once drained, neither operation is allowed.

## Create a draft invoice

<CodeGroup>
  ```ts TypeScript theme={null}
  const invoice = await ionic.invoices.create({
    "Idempotency-Key": "inv_feb_cus_abc",
    customer: "cus_000000000000000000000000",
    currency: "usd",
    collection_method: "charge_automatically",
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/invoices \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: inv_feb_cus_abc" \
    -H "Content-Type: application/json" \
    -d '{
      "customer": "cus_000000000000000000000000",
      "currency": "usd",
      "collection_method": "charge_automatically"
    }'
  ```
</CodeGroup>

| Field               | Type    | Notes                                                                                                                                           |
| ------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `customer`          | string  | Required. Customer id (`cus_…`).                                                                                                                |
| `currency`          | string  | Required. Three-letter ISO currency code.                                                                                                       |
| `collection_method` | string  | Required. `charge_automatically` charges the customer's saved card off-session. `send_invoice` generates a hosted invoice for the buyer to pay. |
| `due_date`          | integer | Unix epoch timestamp. Set when using `send_invoice` to express payment terms.                                                                   |
| `metadata`          | object  | Key-value data attached to the invoice.                                                                                                         |

The created invoice has `status: draft` and no `number`. Any pending invoice items for this customer in the given currency are drained onto the draft atomically.

## Edit the draft

Add or remove lines while the invoice is a draft. Each call recomputes `subtotal` and `total`.

**Add a line**

<CodeGroup>
  ```ts TypeScript theme={null}
  const line = await ionic.invoices.addLine({
    id: "inv_000000000000000000000000",
    "Idempotency-Key": "line_setup_feb",
    description: "Setup fee",
    quantity: 1,
    unit_amount: 5000,
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/invoices/inv_000000000000000000000000/lines \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: line_setup_feb" \
    -H "Content-Type: application/json" \
    -d '{
      "description": "Setup fee",
      "quantity": 1,
      "unit_amount": 5000
    }'
  ```
</CodeGroup>

| Field         | Type    | Notes                                                             |
| ------------- | ------- | ----------------------------------------------------------------- |
| `description` | string  | Required. Display label for this line.                            |
| `quantity`    | integer | Required. Minimum 1.                                              |
| `unit_amount` | integer | Required. Per-unit price in minor units, in the invoice currency. |
| `price`       | string  | Optional price id (`price_…`) for catalog reference.              |

**Remove a line**

<CodeGroup>
  ```ts TypeScript theme={null}
  await ionic.invoices.removeLine({
    id: "inv_000000000000000000000000",
    line_id: "il_000000000000000000000000",
  });
  ```

  ```bash curl theme={null}
  curl -X DELETE \
    https://api.ionicfi.com/v1/invoices/inv_000000000000000000000000/lines/il_000000000000000000000000 \
    -H "Authorization: Bearer sk_v1_test_..."
  ```
</CodeGroup>

Both operations return 422 if the invoice is not a draft.

## Finalize

Finalizing transitions the invoice from `draft` to `open`. It assigns the next gapless, per-merchant sequential number (`INV-000001` in live mode, `TEST-INV-000001` in test mode), applies tax, stamps `finalized_at`, and freezes all lines.

<CodeGroup>
  ```ts TypeScript theme={null}
  const finalized = await ionic.invoices.finalize({
    id: "inv_000000000000000000000000",
    "Idempotency-Key": "finalize_inv_feb",
    tax: 490,
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/invoices/inv_000000000000000000000000/finalize \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: finalize_inv_feb" \
    -H "Content-Type: application/json" \
    -d '{ "tax": 490 }'
  ```
</CodeGroup>

| Field | Type    | Notes                                                              |
| ----- | ------- | ------------------------------------------------------------------ |
| `tax` | integer | Tax amount in minor units. Defaults to 0 when the body is omitted. |

<Note>
  A zero-total invoice (all lines sum to zero plus zero tax) transitions directly to `paid` at finalization. No collection call is needed.
</Note>

Abandoned drafts never consume a number. The finalized sequence is always contiguous.

## Collect payment

### charge\_automatically

Call `POST /v1/invoices/{id}/pay` to charge the customer's saved card off-session. Always include an `Idempotency-Key` — retrying without one can create a duplicate charge.

<CodeGroup>
  ```ts TypeScript theme={null}
  const paid = await ionic.invoices.pay({
    id: "inv_000000000000000000000000",
    "Idempotency-Key": "pay_inv_feb_attempt_1",
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/invoices/inv_000000000000000000000000/pay \
    -H "Authorization: Bearer sk_v1_test_..." \
    -H "Idempotency-Key: pay_inv_feb_attempt_1"
  ```
</CodeGroup>

| Response | Meaning                                                                                                                 |
| -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `200`    | Invoice is now `paid`.                                                                                                  |
| `402`    | Card declined. Check `error.code` for the decline reason.                                                               |
| `422`    | Invoice is not open or not `charge_automatically`, or the customer has no payment method on file (`NO_PAYMENT_METHOD`). |
| `504`    | The payment outcome is not known yet. Retrieve the invoice or its payment attempts before retrying.                     |

<Warning>
  A `504` does not prove the payment failed. Do not retry with a new
  `Idempotency-Key`; the payment may already have been authorized. Retrieve the
  invoice or its payment attempts, and reuse the same key for the same request.
</Warning>

The invoice's `payment_intent` field links to the payment intent created for collection (`pi_…`). During a dunning retry cycle, `next_payment_attempt_at` shows the Unix timestamp of the next scheduled attempt.

### send\_invoice

When `collection_method` is `send_invoice`, Ionic provides a hosted invoice page where the buyer enters their payment details. No server-side `pay` call is needed; the invoice transitions to `paid` when the buyer completes payment on the hosted page.

<Tip>
  Subscribe to `invoice.paid` and `invoice.payment_failed` to handle collection
  results regardless of how the invoice was paid. See [Webhook
  events](/webhooks/events). Event delivery can occur shortly after the API
  response.
</Tip>

## Void and write off

**Void** cancels an open invoice that has taken no payment.

<CodeGroup>
  ```ts TypeScript theme={null}
  const voided = await ionic.invoices.void({
    id: "inv_000000000000000000000000",
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/invoices/inv_000000000000000000000000/void \
    -H "Authorization: Bearer sk_v1_test_..."
  ```
</CodeGroup>

**Mark uncollectible** writes off an open invoice as bad debt. The outstanding balance is retained for reporting.

<CodeGroup>
  ```ts TypeScript theme={null}
  const uncollectible = await ionic.invoices.markUncollectible({
    id: "inv_000000000000000000000000",
  });
  ```

  ```bash curl theme={null}
  curl -X POST https://api.ionicfi.com/v1/invoices/inv_000000000000000000000000/mark_uncollectible \
    -H "Authorization: Bearer sk_v1_test_..."
  ```
</CodeGroup>

Both operations return 422 if the invoice is not in `open` status.

## Invoice statuses

| Status          | Meaning                                                      |
| --------------- | ------------------------------------------------------------ |
| `draft`         | Editable. Lines can be added or removed. No number assigned. |
| `open`          | Finalized and awaiting payment.                              |
| `paid`          | Payment collected in full.                                   |
| `void`          | Cancelled before any payment was taken.                      |
| `uncollectible` | Written off. Outstanding balance retained for reporting.     |

## Payment attempts

Retrieve an invoice's payment attempts, newest first. Use this history to show
each attempt and its result.

<CodeGroup>
  ```ts TypeScript theme={null}
  const attempts = await ionic.invoices.listPaymentAttempts({
    id: "inv_000000000000000000000000",
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/invoices/inv_000000000000000000000000/payment_attempts \
    -H "Authorization: Bearer sk_v1_test_..."
  ```
</CodeGroup>

Each attempt has a `status` of `pending`, `succeeded`, `failed`, or `blocked`,
and a `failure_code` when the card was declined or blocked. `pending` means the
final result is not known yet; retrieve the attempts again before retrying.

## Retrieve and list

<CodeGroup>
  ```ts TypeScript theme={null}
  const retrieved = await ionic.invoices.retrieve({
    id: "inv_000000000000000000000000",
  });
  ```

  ```bash curl theme={null}
  curl https://api.ionicfi.com/v1/invoices/inv_000000000000000000000000 \
    -H "Authorization: Bearer sk_v1_test_..."
  ```
</CodeGroup>

<CodeGroup>
  ```ts TypeScript theme={null}
  const invoices = await ionic.invoices.list({
    customer: "cus_000000000000000000000000",
    status: "open",
    limit: 20,
  });
  ```

  ```bash curl theme={null}
  curl "https://api.ionicfi.com/v1/invoices?customer=cus_000000000000000000000000&status=open&limit=20" \
    -H "Authorization: Bearer sk_v1_test_..."
  ```
</CodeGroup>

The list endpoint accepts `customer`, `subscription`, `status`, `limit` (default 20, max 100), and `offset`.

## Best practices

* Always send an `Idempotency-Key` on `POST /v1/invoices/{id}/pay`. A network timeout leaves the charge state ambiguous — retrying with the same key is safe; retrying without one risks a duplicate.
* Distinguish `402` from `504` before deciding what to do next. A `402` is a definitive decline and requires a new payment method. A `504` means the outcome is unknown and will resolve on its own.
* Use `GET /v1/invoices/{id}/payment_attempts` to show each collection try with
  its outcome and failure code.
* Store your order or billing ID in `metadata` when creating the invoice so you
  can match it to records in your own system.
* Record metered or usage-based charges as invoice items throughout the billing period. When the billing cycle closes, create one invoice per customer and currency — the pending items drain automatically.
* A `409` from any mutating endpoint means the invoice was modified concurrently. The request can be retried as-is.
