# 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`
- `client_id`
- `issued_from`, `issued_to` — date range on `issue_date`
- `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.

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

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

---

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