# Invoices & payments

An invoice bills a job's customer. TruTec has two kinds:

| `payment_method` | What it is | Through the API |
|---|---|---|
| `check` | A manual invoice the customer pays by check, ACH or another way you record. Statuses `draft`, `sent`, `paid`, `void`. | Read, create, edit, send, mark sent, mark paid, void, back to draft. |
| `stripe` | An online invoice the customer pays by card or bank. Statuses `open`, `paid`, `void`, `uncollectible`, set only by the payment processor. | Read, and create from an estimate with [Request payment online](#request-payment-online). |

The API never charges a card. Invoice objects never carry the customer's payment link; the one place it is returned is the response of [Request payment online](#request-payment-online).

Invoices you create start as **drafts**. A draft is not emailed and is not posted to accounting. Use `/send` to email it, or `/mark-sent` when you deliver it another way. When the workspace is connected to QuickBooks Online or another accounting system, the invoice and its payment sync on those steps exactly as in TruTec.

Money is in dollars with two decimals. The job invoice list (`GET /v1/jobs/{id}/invoices`, see [Job costing](job-costing.md)) returns the same rows without the contents.

## The invoice object

| Field | Type | Description |
|---|---|---|
| `id`, `job_id` | integer | |
| `doc_number` | integer or null | The estimate's document number, for invoices billed from an estimate. |
| `status`, `payment_method`, `billing_type` | string | `billing_type` is `full`, `deposit`, `milestone`, `custom`, `line_items` or `recurring`. |
| `description` | string | First line item, or "N line items". |
| `line_items` | array | `{description, amount}`. |
| `subtotal`, `discount`, `tax`, `amount` | number | `amount` = subtotal − discount + tax. Each also comes in cents (`amount_cents` and so on). |
| `due_date`, `payment_terms` | | For example `Net 30` or `Due on receipt`. |
| `issued_at`, `paid_at`, `updated_at`, `created_at` | string | UTC. |
| `client_name`, `client_email`, `client_address` | | Who is billed. |
| `project_name`, `project_address`, `reference_number`, `memo` | | |
| `remit_to_name`, `remit_to_address`, `payment_instructions` | | Where and how to pay. |
| `sender_name`, `sender_email`, `created_by` | | Who it's from. |
| `estimate_id` | integer or null | The estimate it bills. |
| `quickbooks` | object | `has_invoice`, `has_payment`, `invoice_synced_at`, `sync_error`. |

## Invoices

### List invoices

`GET /v1/invoices`

**Permission:** Read jobs

Newest first. With `updated_after`, sorted by `updated_at`, oldest first.

| Name | Type | Required | Description |
|---|---|---|---|
| `job_id` | integer | No | |
| `status` | string | No | |
| `payment_method` | string | No | `check` or `stripe`. |
| `updated_after` | string | No | ISO 8601. |
| `limit`, `offset` | integer | No | Default 50, maximum 200. |

```bash
curl "https://trutec.ai/api/v1/invoices?job_id=70412" \
  -H "X-API-Key: sk_your_api_key_here"
```

```json
{
  "data": [
    {
      "id": 7790,
      "job_id": 70412,
      "doc_number": null,
      "status": "draft",
      "billing_type": "line_items",
      "payment_method": "check",
      "description": "2 line items",
      "currency": "usd",
      "amount_cents": 1405050,
      "amount": 14050.5,
      "subtotal_cents": 1395050,
      "subtotal": 13950.5,
      "discount_cents": 0,
      "discount": 0.0,
      "tax_cents": 10000,
      "tax": 100.0,
      "due_date": "2026-11-26T15:02:11Z",
      "issued_at": "2026-11-11T15:02:11Z",
      "paid_at": null,
      "updated_at": "2026-11-11T15:02:11Z",
      "quickbooks": { "has_invoice": false, "has_payment": false, "invoice_synced_at": null, "sync_error": null },
      "line_items": [
        { "description": "Sealcoat, 2 coats", "amount": 12500.0 },
        { "description": "Striping", "amount": 1450.5 }
      ],
      "client_name": "Northgate Property Group",
      "client_email": "ap@northgate.example",
      "client_address": null,
      "project_name": "Northgate Plaza reseal",
      "project_address": "1200 Commerce Dr, Austin, TX 78701",
      "reference_number": "PO-5521",
      "memo": "Thank you for your business.",
      "payment_terms": "Net 15",
      "remit_to_name": "Northgate Paving",
      "remit_to_address": "400 Industrial Way, Austin, TX 78702",
      "payment_instructions": null,
      "sender_name": "Northgate Paving",
      "sender_email": "billing@northgatepaving.example",
      "estimate_id": null,
      "created_by": { "id": 12, "name": "Office Manager" },
      "created_at": "2026-11-11T15:02:11Z"
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}
```

### Get an invoice

`GET /v1/invoices/{id}`

**Permission:** Read jobs

### Create a draft invoice

`POST /v1/invoices`

**Permission:** Write jobs

Creates a manual (`check`) invoice on a job as a draft. The customer name and email default from the job's bill-to, then its company and contact. The remit-to address defaults to the last one the workspace used.

| Name | Type | Required | Description |
|---|---|---|---|
| `job_id` | integer | Yes | |
| `line_items` | array | Yes | `[{"description": "...", "amount": 1250.00}]`, amounts in dollars, above 0. |
| `client_name`, `client_email`, `client_address` | string | No | Override the job's billing contact. |
| `billing_type` | string | No | `line_items` (default), `full`, `deposit`, `milestone` or `custom`. |
| `due_days` | integer | No | Days until due. Default 30; 0 is due on receipt. |
| `discount`, `tax` | number | No | Dollars. The discount can't exceed the subtotal. |
| `memo`, `reference_number`, `project_name`, `project_address` | string | No | |
| `remit_to_name`, `remit_to_address`, `payment_instructions` | string | No | A remit-to address is required if the workspace has none on file. |
| `estimate_id` | integer | No | The job's estimate this invoice bills. |

```bash
curl -X POST "https://trutec.ai/api/v1/invoices" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "job_id": 70412,
    "line_items": [
      { "description": "Sealcoat, 2 coats", "amount": 12500 },
      { "description": "Striping", "amount": 1450.50 }
    ],
    "tax": 100,
    "due_days": 15,
    "reference_number": "PO-5521"
  }'
```

Returns `201` with the invoice.

**Errors:** `400` for missing or invalid line items, a missing remit-to address, or a total of zero. `404` when the job or estimate is not in your workspace.

### Update an invoice

`PATCH /v1/invoices/{id}`

**Permission:** Write jobs

Changes the fields you send (the same fields as **Create**, except `job_id` and `estimate_id`). `line_items` replaces the whole list. The due date stays anchored to the issue date. If the invoice is already in QuickBooks Online, the edit is pushed there too. Online invoices (`stripe`) return `409`; invoices imported from another system keep their original figures (`409`).

### Send an invoice

`POST /v1/invoices/{id}/send`

**Permission:** Write jobs

**This emails the customer** (`client_email`) the invoice with a link to view it, from the workspace's designated invoice sender, or else from the key creator's connected mailbox. A draft becomes `sent` and, on first send, is created in the connected accounting system. You can send again to resend. Returns `200` with `emailed: true`.

**Errors:** `422` when the invoice has no `client_email`. `409` for a void or online invoice. `502` if the email could not be delivered.

### Mark sent, mark paid, or void

`POST /v1/invoices/{id}/mark-sent`

`POST /v1/invoices/{id}/mark-paid`

`POST /v1/invoices/{id}/void`

**Permission:** Write jobs

Nothing is emailed. `mark-sent` is for invoices you delivered yourself (it also moves a paid invoice back to unpaid). `mark-paid` records the payment today and posts it to the connected accounting system. `void` voids the invoice there too. Online invoices return `409`: their status follows the payment processor.

### Request payment online

`POST /v1/estimates/{id}/request-payment`

**Permission:** Write jobs (the key's user needs pricing access)

Bills an estimate with an online invoice, exactly like **Create invoice** with the Online (card / bank) method on the estimate's Billing tab. The amount, bill-to, due date, accounting sync and double-billing guard are TruTec's own.

!!! warning
    This **emails the customer**. The workspace's connected payments account emails them a page to pay by card or bank right away. Confirm the amount and the customer before calling it.

| Name | Type | Description |
|---|---|---|
| `kind` | string | `full` (the contract, the default), `deposit` (the proposal's deposit percent), `progress` (work done so far minus what's already billed) or `itemized` (one line per item). |
| `due_days` | integer | Days until due. Default: the proposal's payment terms. |
| `memo`, `reference_number` | string | `reference_number` is the customer's PO number. |
| `client_name`, `client_email` | string | Who is billed and where the invoice is emailed. Default: the job's bill-to, else the proposal's customer. |
| `confirm_duplicate` | boolean | Bill anyway after a `409` double-billing refusal. |

Needs online payments connected in TruTec (Settings › Payments; `409` `Online payments not set up` otherwise) and a prepared proposal (`409` `No proposal yet`; see [Proposals](proposals.md)). The same invoice again within 10 minutes, or a full-contract invoice that would bill more than the contract, is refused with `409` and `code` `duplicate_invoice` or `over_contract` unless you send `confirm_duplicate: true`.

```bash
curl -X POST "https://trutec.ai/api/v1/estimates/9840/request-payment" \
  -H "X-API-Key: sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"kind": "deposit"}'
```

```json
{
  "data": {
    "invoice": {"id": 9120, "job_id": 70412, "status": "open", "payment_method": "stripe", "billing_type": "deposit", "amount": 12125.0, "...": "..."},
    "payment_url": "https://invoice.stripe.com/i/acct_example/test_example",
    "emailed": true,
    "message": "Invoice created and submitted to Stripe for email delivery."
  }
}
```

Returns `201`. `emailed` is false when the invoice was made but the payments account couldn't confirm the email: share `payment_url` yourself rather than requesting again. The invoice's status follows the payment processor (webhook `payment_received` when paid). Mailed-check invoices: [Create a draft invoice](#create-a-draft-invoice).

## Payments

### List payments

`GET /v1/payments`

**Permission:** Read jobs

One row per paid invoice, most recent first. TruTec records a payment when an invoice is marked paid, or when the customer pays an online invoice. Partial payments are not tracked.

| Name | Type | Required | Description |
|---|---|---|---|
| `job_id` | integer | No | |
| `paid_after` | string | No | ISO 8601. |
| `limit`, `offset` | integer | No | |

```json
{
  "data": [
    {
      "invoice_id": 7781,
      "job_id": 70412,
      "doc_number": 1412,
      "amount": 14550.0,
      "currency": "usd",
      "paid_at": "2026-10-18T19:02:13Z",
      "payment_method": "check",
      "client_name": "Northgate Property Group",
      "quickbooks": { "has_payment": true, "payment_synced_at": "2026-10-18T19:02:40Z" }
    }
  ],
  "pagination": { "total": 1, "limit": 50, "offset": 0 }
}
```

## Back to draft

### Move an invoice back to draft

`POST /v1/invoices/{id}/mark-draft`

**Permission:** Write jobs

The same as choosing **Draft** in the invoice's status menu in TruTec. A void invoice is no longer void, and a paid invoice loses its payment date (it drops out of `GET /v1/payments`). Nothing is emailed. Use it to undo a `mark-sent`, `mark-paid` or `void` made by mistake, then edit the invoice and send it again.

If the invoice is already in the connected accounting system, that copy stays there: marking the invoice sent again pushes your edits to it (a copy that was voided there is replaced with a new one). A payment already posted there is not removed; remove it in the accounting system.

```bash
curl -X POST "https://trutec.ai/api/v1/invoices/7790/mark-draft" \
  -H "X-API-Key: sk_your_api_key_here"
```

Returns `200` with the invoice, `status` `draft`.

**Errors:** `400` when the invoice is linked to another invoicing system and was already sent or paid. `409` for an online invoice. `404` when the invoice is not in your workspace.

### Deleting invoices

Invoices can't be deleted, in TruTec or through the API. Void one instead (`POST /v1/invoices/{id}/void`): it stays on the job for your records and is voided in the connected accounting system too.
