# OrderOS API Reference

> **Machine-readable spec:** [openapi.yaml](https://app.orderos.sk/openapi.yaml). Drop into Anthropic tool-use, ChatGPT actions, n8n, Postman, etc. for auto-generated tool definitions.
>
> **Human reference:** [api-reference.md](https://app.orderos.sk/api-reference.md). Same content as this page — fetchable directly without Confluence access.
>
> **Response shape:** all responses are wrapped — single resources as `{ "data": { ... } }`, collections as `{ "data": [...], "meta": {...} }`. Always read `response.data` for the record fields.



REST API for agents and integrations. Tenant-scoped via `X-Tenant-Id` header.
Auth via Sanctum personal access tokens.

Base URL: `https://app.orderos.sk/api`

## Versioning

Every `/api/*` response includes an `X-API-Version` header — a 12-char hash of the current OpenAPI spec. Cache the value on first request; if a later response shows a different value, the contract changed.

### `GET /api/version`

Public (no auth). Returns the current API version plus URLs to the spec and human reference. Use this at agent startup and whenever `X-API-Version` changes mid-session.

```json
{
  "data": {
    "version": "a3f9c1e2b7d4",
    "spec_url": "https://app.orderos.sk/openapi.yaml",
    "human_reference_url": "https://app.orderos.sk/api-reference.md"
  }
}
```

**Recommended agent loop:**
1. On boot: `GET /api/version` → fetch `spec_url` → cache the version.
2. On every API response: compare `X-API-Version` header to cached value. If different, refetch `/api/version`, refetch the spec, refresh tool definitions, retry.
3. On any unexpected `4xx` (e.g. validation error on a previously-working call): treat as a possible contract change and refresh as in step 2.

## Authentication

### `POST /api/login`

Body:
```json
{ "email": "you@example.com", "password": "secret", "device_name": "my-agent" }
```

Response:
```json
{
  "token": "1|abc...",
  "user": { "id": 1, "name": "Sam", "email": "...", "is_super_admin": false },
  "tenants": [ { "id": 5, "name": "Appticore", "slug": "appticore" } ]
}
```

Use the returned `token` as `Authorization: Bearer <token>` on all subsequent requests. `device_name` is just a label so you can revoke individual tokens later.

### Required headers for tenant-scoped endpoints

```
Authorization: Bearer <token>
X-Tenant-Id: <tenant id from /login response>
Accept: application/json
Content-Type: application/json
```

If `X-Tenant-Id` is omitted, the user's `last_tenant_id` is used.

### `POST /api/logout`

Revokes the current token.

---

## Clients

### `GET /api/clients`

Query params:
- `search` — matches `company_name`, `contact_name`, `email`, `ico`
- `per_page` — default 25, max 100
- `page` — pagination

Response: paginated list of `ClientResource`.

### `GET /api/clients/{id}`

Single client.

### `POST /api/clients`

Body (any fields, all optional except company_name OR contact_name):
```json
{
  "company_name": "Acme s.r.o.",
  "contact_name": "Jana Nováková",
  "email": "jana@acme.sk",
  "phone": "+421...",
  "address": "Hlavná 1",
  "zip": "811 01",
  "city": "Bratislava",
  "country": "Slovensko",
  "ico": "12345678",
  "dic": "1234567890",
  "ic_dph": "SK1234567890",
  "iban": "SK0000...",
  "swift": "GIBASKBX",
  "bank_account": "...",
  "language": "de"
}
```

Returns 201 + the new ClientResource.

Permission required: `clients.create`.

---

## Products

### `GET /api/products?search=&per_page=`

Lookup products by name or SKU. Use `id` as `product_id` when adding invoice line items.

Permission: `products.view`.

---

## Invoices

### `GET /api/invoices`

Query params:
- `status` — `draft`, `issued`, `sent`, `paid`, `partially_paid`, `cancelled`, or virtual `overdue` (= unpaid + due_date in past)
- `type` — `invoice`, `proforma`, `credit_note`, `quote`
- `client_id`
- `issued_from`, `issued_to` — date range on `issue_date`
- `search` — matches `invoice_number` and the invoice's `client_name` snapshot
- `per_page`, `page`

Sorted by `invoice_number` desc by default.

### `GET /api/invoices/{id}`

Returns the invoice with `items[]` included.

### `POST /api/invoices`

Body:
```json
{
  "client_id": 7,
  "type": "invoice",
  "issue_date": "2026-05-05",
  "due_date": "2026-05-19",
  "currency": "EUR",
  "language": "de",
  "name": "SLA April 2026",
  "payment_method": "transfer",
  "status": "issued",
  "notes": "...",
  "order_ids": [15, 18],
  "items": [
    { "product_id": 12, "quantity": 2 },
    { "name": "Custom service", "quantity": 1, "unit_price": 500, "tax_rate": 23, "unit": "hod" }
  ]
}
```

Each item must have `product_id` OR (`name` + `quantity` + `unit_price` + `tax_rate`). When `product_id` is given, missing fields fall back to the product's defaults.

`order_ids` (optional) links the invoice to one or more of the client's orders. Each order must belong to the client and must not be linked to another invoice yet (`422` otherwise). Linked order ids are returned as `order_ids` on the invoice resource; deleting the invoice frees the orders for billing again. Note: `order_ids` only creates the link — the invoice lines still come from `items`, so the caller composes them (merge duplicate products yourself if billing multiple orders).

`status` (optional) is `issued` (default) or `draft`. A **draft** is created **without an invoice number** (`invoice_number` is `null`) and consumes no numbering counter — the number is assigned only when the draft is issued (see `POST /api/invoices/{id}/issue`). A draft cannot be sent or rendered to PDF (both return `422`). Draft PDFs are unavailable because the document has no number yet.

`type: "quote"` creates a quote (cenova ponuka). Quotes accept an optional `valid_until` (date, ignored for other types), have no due date and are never payable: `POST /api/invoices/{id}/mark-paid` on a quote returns `422`. The invoice object exposes `valid_until` (date or `null`) and `converted_to_invoice_id` (id of the proforma/invoice created from the quote, otherwise `null`). Creating a quote fires the `invoice.created` webhook like any other document.

`language` selects the document language for the PDF and the email (sk, en, de, cs). When omitted, the API falls back to the client's default language, then to the tenant's default.

`name` is an optional internal label for the invoice (e.g. "SLA April 2026", "Web build"). Shown in lists and on the invoice show page; not printed on the PDF or used as the customer-facing number.

Returns 201 + InvoiceResource (items included).

Permission: `invoices.create`.

### `POST /api/invoices/{id}/issue`

Issues a draft: assigns its invoice number and transitions `draft` → `issued`. Returns `422` if the invoice is not a draft.

Body (all optional):
```json
{
  "issue_date": "2026-08-03",
  "invoice_number": "001082026"
}
```

- `issue_date` — defaults to the draft's stored issue date, or today. Drives the number when the format uses date tokens (e.g. `{MONTHSEQ}` picks the counter for this date's month).
- `invoice_number` — an explicit number. When omitted, the next number is generated from the tenant's format and counters. Returns `422` if the number is already in use for this type.

Returns InvoiceResource. Permission: `invoices.edit`.

### `POST /api/invoices/{id}/send`

Body (all optional):
```json
{
  "email": "override@example.com",
  "subject": "Faktúra na úhradu — apríl 2026",
  "body": "Dobrý deň,\n\nv prílohe zasielame faktúru.\n\nĎakujeme,\nSam",
  "bcc": ["accounting@us.sk", "owner@us.sk"]
}
```

- `email` — recipient. Defaults to the client's current contact email (or the invoice snapshot if the client was deleted).
- `subject` — overrides the default subject (`Faktúra <number> — <company>`).
- `body` — custom message text shown above the standard email content; supports newlines, no HTML.
- `bcc` — list of BCC addresses.

Renders PDF, sends, and transitions `issued` → `sent`. Permission: `invoices.send`.

### `POST /api/invoices/{id}/mark-paid`

Body (both optional):
```json
{ "amount": 615.00, "paid_at": "2026-05-05T10:00:00Z" }
```

If `amount` is omitted, marks fully paid. Sets status to `paid` or `partially_paid` based on running paid total.

Permission: `invoices.edit`.

---

### `GET /api/invoices/{id}/pdf`

Returns the PDF binary inline (`Content-Type: application/pdf`). Permission: `invoices.view`.

### `POST /api/invoices/{id}/cancel`

Sets status to `cancelled`. Permission: `invoices.edit`.

### `POST /api/invoices/{id}/unmark-paid`

Clears `paid_amount` and `paid_at`, sets status back to `issued`. Use when a payment was recorded by mistake. Permission: `invoices.edit`.

### `POST /api/invoices/{id}/duplicate`

Creates a new **draft** invoice (no number until issued) with the same client snapshot, items, language, currency, and notes. Issue it with `POST /api/invoices/{id}/issue`. Returns the new invoice (201). Permission: `invoices.create`.

### `POST /api/invoices/{id}/convert`

Converts a **quote** into a proforma or an invoice. Body:
```json
{ "target": "invoice" }
```

`target` is `proforma` or `invoice`. Creates the child document from the quote (same client snapshot and items), links it via the quote's `converted_to_invoice_id` and returns the new document as an invoice object (`201`). Errors: `404` when `{id}` is not a quote, `422` when `target` is invalid or the quote cannot be converted (for example it was already converted; the response `message` explains why). Permission: `invoices.create`.

### `DELETE /api/invoices/{id}`

Hard-deletes the invoice and reclaims its number (counter rolls back when it was the most recently issued). **Only allowed when status is `cancelled`.** For any other status (issued, sent, paid, partially_paid, overdue) use `POST /api/invoices/{id}/cancel` first, then delete. Returns `422` with a JSON message if the status doesn't allow deletion. Deleting an invoice also unlinks its orders, making them billable again. Permission: `invoices.delete`.

### `POST /api/invoices/bulk-mark-paid`

Body:
```json
{ "invoices": [
  { "id": 12, "amount": 615.00, "paid_at": "2026-05-05" },
  { "id": 14 }
] }
```

Returns:
```json
{ "ok": [ { "id": 12, "status": "paid" }, ... ], "failed": [ { "id": 99, "error": "not found" } ] }
```

Up to 200 invoices per call. Permission: `invoices.edit`.

---

## Recurring Invoices

Templates that the daily 06:00 cron uses to auto-generate invoices.

### `GET /api/recurring-invoices`

Query params: `client_id`, `frequency` (e.g. `monthly`, `3_months`, `yearly`), `active_only=1`, `search` (matches name/description), `per_page`, `page`. Sorted by `next_issue_date` asc.

### `GET /api/recurring-invoices/{id}`

Single template with `items[]` and `client_name`.

### `POST /api/recurring-invoices`

```json
{
  "client_id": 7,
  "name": "SLA quarterly",
  "frequency": "3_months",
  "start_date": "2026-07-01",
  "next_issue_date": "2026-07-01",
  "end_date": null,
  "day_of_month": 1,
  "due_days": 14,
  "payment_method": "transfer",
  "currency": "EUR",
  "description": "Vystavujeme Vám faktúru za:",
  "notes": null,
  "auto_issue": true,
  "auto_send": false,
  "send_to_emails": "billing@client.sk",
  "is_active": true,
  "items": [
    { "name": "Quarterly retainer", "unit_price": 500, "tax_rate": 23, "quantity": 1, "unit": "" }
  ]
}
```

Frequencies: `none`, `weekly`, `2_weeks`, `monthly`, `2_months`, `3_months`, `4_months`, `6_months`, `yearly`, `2_years`, `4_years`. `quantity` and `unit` are optional. Permission: `invoices.create`.

### `PUT /api/recurring-invoices/{id}`

Same body shape; pass only fields you want to change. If `items` is included, the full item set is replaced. Permission: `invoices.edit`.

### `POST /api/recurring-invoices/{id}/toggle`

Flips `is_active` (pause / resume). Returns the updated template. Permission: `invoices.edit`.

### `DELETE /api/recurring-invoices/{id}`

Removes the template and its items. Generated invoices remain. Permission: `invoices.delete`.

---

## Orders

### `GET /api/orders`

Query params:
- `status` — `draft`, `pending`, `completed`, `cancelled`
- `client_id`
- `created_from`, `created_to` — date range on `created_at`
- `search` — matches `order_number`, client `company_name`, client `contact_name`
- `per_page` — default 25, max 100
- `page` — pagination

Staff without the `orders.view_all` permission only see orders **assigned to them or unassigned** (`assigned_staff_id` is null) — the same scoping the web UI applies. These endpoints are for staff users; client-portal users are redirected away from all non-portal routes (including this one) and cannot reach it in the current build. Super admins are unaffected by the scoping (they bypass permission checks entirely).

Response: paginated list of order summaries (no `items`, `status_history`, `invoices`, or `delivery_notes` — those are detail-only, see below):
```json
{
  "data": [
    {
      "id": 42,
      "order_number": "ORD-2026-0042",
      "status": "pending",
      "priority": "normal",
      "currency": "EUR",
      "requested_date": "2026-08-20",
      "grand_total": 615.00,
      "is_billed": false,
      "created_at": "2026-08-17T09:00:00+00:00",
      "client": { "id": 7, "company_name": "Acme s.r.o.", "contact_name": "Jana Nováková" },
      "assigned_staff": { "id": 3, "name": "Peter Staff" },
      "items_count": 4
    }
  ],
  "meta": { "current_page": 1, "last_page": 2, "per_page": 25, "total": 34 }
}
```

`grand_total` is **gross** (VAT included). Permission: `orders.view_own`.

### `GET /api/orders/{id}`

Returns the full order with `items`, `status_history`, `invoices`, `delivery_notes`, and `allowed_transitions` included:
```json
{
  "data": {
    "id": 42,
    "order_number": "ORD-2026-0042",
    "status": "pending",
    "priority": "normal",
    "currency": "EUR",
    "requested_date": "2026-08-20",
    "grand_total": 615.00,
    "is_billed": false,
    "created_at": "2026-08-17T09:00:00+00:00",
    "client": { "id": 7, "company_name": "Acme s.r.o.", "contact_name": "Jana Nováková" },
    "assigned_staff": { "id": 3, "name": "Peter Staff" },
    "notes": "Deliver to the back entrance",
    "client_notes": "Please call before arrival",
    "confirmed_date": "2026-08-18",
    "completed_at": null,
    "cancelled_at": null,
    "cancellation_reason": null,
    "subtotal": 615.00,
    "discount_total": 0,
    "items": [
      {
        "id": 101,
        "product_id": 12,
        "product_name": "Delivery service",
        "product_sku": "DEL-01",
        "quantity": 1,
        "unit": "each",
        "unit_price": 500.00,
        "tax_rate": 23,
        "tax_amount": 115.00,
        "discount_amount": 0,
        "line_total": 615.00,
        "notes": null
      }
    ],
    "status_history": [
      { "from_status": null, "to_status": "draft", "comment": null, "changed_by_user_id": 3, "created_at": "2026-08-15T08:00:00+00:00" },
      { "from_status": "draft", "to_status": "pending", "comment": null, "changed_by_user_id": 3, "created_at": "2026-08-15T08:05:00+00:00" }
    ],
    "invoices": [],
    "delivery_notes": [],
    "allowed_transitions": ["completed", "cancelled"]
  }
}
```

`subtotal` and every `items[].line_total` are **gross** (net + VAT − item discount); `tax_amount` is stored for reference only, not added on top. `allowed_transitions` lists the statuses this order can move to from its current status, already filtered for whether the caller is a client or staff/agent (mirrors the web UI's status dropdown). Permission: `orders.view_own`. Staff without `orders.view_all` get a `403` here (not a silent omission) if the order is assigned to a different staff member — unlike the list endpoint's filtering, an order id assigned to someone else is not viewable at all unless it's unassigned or assigned to the caller.

### `POST /api/orders/{id}/status`

Body:
```json
{ "status": "completed", "comment": "Delivered and signed off" }
```

- `status` — required, one of `draft`, `pending`, `completed`, `cancelled`.
- `comment` — optional, max 1000 chars. Recorded on the status history entry.
- `cancellation_reason` — **required when `status` is `cancelled`**, max 1000 chars. Stored on the order and used as the history comment (any `comment` value is ignored for a cancellation).

Valid transitions: `draft → pending`, `draft → cancelled`, `pending → completed`, `pending → cancelled`, `cancelled → pending`. There is no way back out of `completed`. Attempting a transition not in this list returns `422`:
```json
{ "message": "Cannot transition order from 'completed' to 'pending'." }
```

Completing an order (`pending → completed`) auto-creates its delivery note and emails it to the client — identical to clicking "Complete" in the web UI. This happens asynchronously after the response is sent, so the response itself does not include the new delivery note; fetch `GET /api/orders/{id}` afterwards if you need its id/number.

Returns the updated order in the same shape as `GET /api/orders/{id}`. Permission: `orders.change_status`.

---

## Dashboard

### `GET /api/dashboard`

Summary counters and recent activity for the current tenant, scoped by the caller's permissions.

```json
{
  "data": {
    "orders": { "today": 3, "pending": 7 },
    "invoices": { "unpaid_count": 5, "unpaid_total": 2340.50, "overdue_count": 2, "overdue_total": 890.00 },
    "recent_orders": [
      { "id": 42, "order_number": "ORD-2026-0042", "status": "pending", "grand_total": 615.00, "client_name": "Acme s.r.o.", "created_at": "2026-08-17T09:00:00+00:00" }
    ],
    "recent_invoices": [
      { "id": 12, "invoice_number": "2026080", "status": "issued", "grand_total": 615.00, "client_name": "Acme s.r.o.", "due_date": "2026-08-31" }
    ]
  }
}
```

- `orders` is `null` if the caller lacks `orders.view_own`; `invoices` is `null` if the caller lacks `invoices.view`. `recent_orders`/`recent_invoices` are empty arrays (not omitted) when the matching block is unavailable. `403` is only returned if the caller has **neither** permission.
- `orders.pending` respects the same assigned/unassigned scoping as `GET /api/orders` for staff without `orders.view_all`.
- `invoices.unpaid_total` sums `grand_total` for every non-draft, non-paid, non-cancelled invoice — **including partially-paid ones counted at their full `grand_total`, not the remaining balance**. This is a v1 approximation; do not treat it as the actual amount outstanding. `overdue_total` is the same query further filtered to `due_date` in the past.
- `recent_orders`/`recent_invoices` are capped at 5, most recent first.

Permission: `orders.view_own` and/or `invoices.view` (at least one required).

---

## Devices

Device tokens power **mobile push notifications for new orders** — when an order moves to `pending`, active staff with a registered device get a push via FCM, in addition to the existing email notification. Tokens are stored centrally (not per-tenant), scoped to the authenticated user and the tenant active at registration time (`X-Tenant-Id`).

### `POST /api/devices`

Body:
```json
{ "token": "fcm-device-token-string", "platform": "ios" }
```

`platform` is `ios` or `android`. Idempotent by `token` — registering the same token again just updates its owner/platform/tenant, it won't create a duplicate. Returns `201`:
```json
{ "message": "Device registered." }
```

### `DELETE /api/devices`

Body:
```json
{ "token": "fcm-device-token-string" }
```

Removes the token (only if it belongs to the caller). Call this on logout / app uninstall to stop push delivery.
```json
{ "message": "Device removed." }
```

---

## Tags

`tags` is an array of short string labels stored on both invoices and clients. Pass on create:
```json
{ ..., "tags": ["retainer", "monthly"] }
```

Filter by tag:
- `GET /api/clients?tag=retainer`
- `GET /api/invoices?tag=retainer`

---

## Webhooks

Tenant-scoped. POST `/api/webhooks` to register a URL that receives signed callbacks for events.

### `GET /api/webhooks`

List registered webhooks (without secret). Permission: `settings.edit`.

### `POST /api/webhooks`

Body:
```json
{
  "url": "https://your-app.example.com/webhook",
  "events": ["invoice.created", "invoice.paid"],
  "description": "agent listener"
}
```

Returns the new webhook including a one-time `secret` field (used to verify signatures — store it; it won't be shown again). Permission: `settings.edit`.

Allowed events: `invoice.created`, `invoice.sent`, `invoice.paid`, `client.created`.

### `DELETE /api/webhooks/{id}`

Removes the webhook.

### Delivery format

The webhook receiver gets a `POST` with:
```
Content-Type: application/json
X-OrderOS-Event: invoice.paid
X-OrderOS-Signature: sha256=<hex hmac of body using your secret>

{
  "event": "invoice.paid",
  "timestamp": "2026-05-05T10:00:00+00:00",
  "data": { ...full InvoiceResource or ClientResource... }
}
```

Verify signature: `hmac_sha256(secret, raw_body) === signature_after_"sha256="`. 10s timeout, no retries; failures increment `failure_count` on the webhook record so you can inspect them via `GET /api/webhooks`.

---

## Errors

- `401` — missing/invalid token
- `403` — token valid but user lacks permission for the action
- `404` — no record (or record belongs to a different tenant)
- `422` — validation failure or invalid state transition (e.g. cancelling an already-cancelled invoice)

Validation errors follow Laravel's standard shape:
```json
{ "message": "...", "errors": { "field": ["error message"] } }
```

All `/api/*` responses — including 404 and 500 — return JSON regardless of the request's `Accept` header. You will never get an HTML error page from the API.

`GET /api/invoices/{id}/pdf` is the exception: success returns binary `application/pdf`, but failures (missing invoice, render error, malformed output) still return JSON with a `message` field. Always check the `Content-Type` header before saving the body as a PDF.

---

## Agent guide

A few rules that aren't obvious from endpoint signatures alone.

### Number / date / decimal format

- Numbers go as JSON numbers: `"unit_price": 615.00` ✅, `"unit_price": "615,00"` ❌. Server-side normalization handles commas inside `items[].*` for safety, but top-level numeric fields expect a real number.
- Dates use ISO 8601: `"issue_date": "2026-05-05"`. Timestamps use RFC 3339: `"paid_at": "2026-05-05T10:00:00Z"`.
- Currency is a 3-letter ISO code: `"EUR"`, `"USD"`. The currency on the invoice is independent of the tenant default.

### Idempotency

There's no `Idempotency-Key` header. To avoid duplicate invoice creation on retry, before retrying a `POST /api/invoices`, query `GET /api/invoices?client_id=X&issued_from=YYYY-MM-DD&issued_to=YYYY-MM-DD` and check whether a near-identical invoice was already created. The same applies to client creation — search by `ico` first.

### Choosing the language for an invoice

Decision tree:
1. If the agent was given an explicit language → use it (`"language": "de"`).
2. Else, fetch the client (`GET /api/clients/{id}`) and use `client.language` if set.
3. Else omit the field; the server falls back to the tenant's default.

### Reverse-charge / VAT-exempt clients

A client is treated as reverse-charge when `client.ic_dph` is set, doesn't start with `SK`, AND VIES has verified it (visible only as a verified state on the client edit page, not directly in the API yet). When the agent creates an invoice for such a client:

- All items should have `tax_rate: 0`.
- The invoice `description` should include the legal note in the chosen language:
  - **sk:** "Dodanie tovaru je oslobodené od dane. Dodanie služby podlieha preneseniu daňovej povinnosti."
  - **de:** "Die Warenzustellung ist von der Umsatzsteuer befreit. Die Lieferung des Dienstes unterliegt der Übertragung der Steuerpflicht."
  - **en:** "The supply of goods is exempt. The supply of services is subject to the reverse charge procedure."
  - **cs:** "Dodání zboží je osvobozeno od daně. Poskytnutí služby podléhá přenesení daňové povinnosti."

### Invoice numbering

The agent does **not** pass `invoice_number` — the system generates one using the tenant's numbering format (`{PREFIX}-{YEAR}-{SEQ:5}` etc.). To continue from a different system after migration, set the format and counter via the Settings UI before calling the API.

### Quantity / unit

`items[].quantity` is optional. Null/zero is treated as 1 for the math. If no item on the invoice has a quantity, the Quantity column is hidden on the rendered PDF (same for Unit). For pure flat-fee items (e.g. "SLA April 2026 — 500 €") just set `name`, `unit_price`, `tax_rate` and skip `quantity` and `unit`.

### Response shape — everything is wrapped in `data`

**Single-resource endpoints** (e.g. `GET /api/invoices/42`, `POST /api/invoices`, action endpoints):

```json
{ "data": { "id": 42, "invoice_number": "2026040", "status": "issued", ... } }
```

**Collection endpoints** (e.g. `GET /api/invoices`):

```json
{
  "data": [ { ... }, { ... } ],
  "meta": { "current_page": 1, "last_page": 5, "per_page": 25, "total": 113 }
}
```

Always parse `response.data.*` for the actual record fields — never the root.

### Suggested system prompt for an invoicing agent

```
You are an invoicing assistant for {tenant_name}. You can call the OrderOS REST
API at https://app.orderos.sk/api with the bearer token {{TOKEN}} and the
header X-Tenant-Id: {{TENANT_ID}}. Numbers must be JSON numbers (no commas).
Dates are ISO-8601.

Before creating a new invoice:
  1. If the user names a client, look it up via GET /api/clients?search=...
     If multiple match, ask the user to clarify. If none match, ask whether to
     create a new client and gather the company_name, ico, ic_dph, address.
  2. Determine the document language:
     - explicit user request, otherwise client.language, otherwise tenant default.
  3. If the client has a non-SK ic_dph, set every item's tax_rate to 0 and
     fill the description with the reverse-charge note in the chosen language.

Invoices are created directly as issued. Only call /send when the user
confirms.

Never invent invoice numbers, IBANs, or VAT IDs. If a number you'd return is
not present in an API response, say so.
```

---

## Common agent workflows

**Create an invoice for an existing client by name:**
1. `GET /api/clients?search=prima banka` → pick `id`
2. `GET /api/products?search=sla` → pick `id` (optional)
3. `POST /api/invoices` with `client_id` + `items` (created directly as issued)
4. `POST /api/invoices/{id}/send` if you want to email it

**Create a new client and immediately invoice them:**
1. `POST /api/clients` → returns new client `id`
2. `POST /api/invoices` with that `client_id`

**Bill several orders with one invoice:**
1. `GET /api/invoices?client_id=X` + your order data → decide which orders to bill
2. `POST /api/invoices` with `client_id`, `order_ids: [..]`, and `items` composed from the orders (merge duplicate products yourself)
3. The linked orders are excluded from future billing until the invoice is deleted

**List unpaid past-due invoices:**
- `GET /api/invoices?status=overdue`
