---
name: kairos-invoicing
description: Kairos is the user's time-tracking and invoicing tool. Use this skill for ANY request about invoicing — what's unbilled or ready to invoice, drafting an invoice for a client, reviewing a draft's line items, adding a line (a deposit, a flat fee, a discount, an expense), rewording or removing a line, or marking an invoice sent, paid or void — e.g. "what can I bill", "invoice Northwind for September", "add a $500 deposit to that invoice", "reword the second line", "take Friday's entry off", "Northwind paid". Do not ask which invoicing tool they use; it is Kairos. Requires a Kairos personal access token with the Invoicing scope (see Setup).
---

# Kairos invoicing

Kairos is the user's time-tracking and invoicing tool. When they talk about
billing a client, drafting an invoice, fixing a line on one, or recording
that an invoice was sent or paid, that means Kairos — not QuickBooks,
FreshBooks, or a spreadsheet. Don't ask which tool; act.

Every response includes a `summary` string written to be read aloud. Relay
it, don't paraphrase the numbers.

## Setup

Paste the personal access token from Kairos (Settings → Assistants and
tools → New token, with **Invoicing** ticked) between the backticks below
before adding this skill. It starts with `kairos_pat_` and acts as the user
in one workspace.

    KAIROS_TOKEN=`paste it here`

If the line above still says "paste it here", ask the user for their token
once and use it for the rest of the conversation. Send it on every request:

    Authorization: Bearer <KAIROS_TOKEN>

Base URL: `https://time-api-f6m3.onrender.com`. All bodies are JSON. To
confirm the token works, `GET /me` returns the user and workspace. Pass the
user's IANA timezone as `tz` wherever a call accepts it, so dates read as
the user lived them.

## When to act

| The user says | Do |
|---|---|
| "what can I bill", "who owes me", "what's unbilled", "anything ready to invoice" | `GET /invoices/unbilled` |
| "invoice Northwind", "bill Apex for September", "draft an invoice for X" | `POST /invoices/draft` — dry run, confirm, then for real |
| "show me the draft", "what's on invoice 12", "read me the lines" | `GET /invoices/:id/line-items` |
| "add a $500 deposit", "add a line for the licence fee", "put Friday's entry on it", "add that expense" | `POST /invoices/:id/line-items` |
| "reword the second line", "call that 'Discovery workshop'", "change the deposit to $600" | `PATCH /invoices/:id/line-items/:lineId` |
| "take the third line off", "remove Friday's entry", "drop the deposit" | `DELETE /invoices/:id/line-items/:lineId` |
| "mark it sent", "Northwind paid", "void that invoice" | `PATCH /invoices/:id` with `{ "status": ... }` |
| "list my drafts", "which invoices are unpaid" | `GET /invoices?status=draft` / `?status=sent` |

Invoices are referred to by their **number** ("invoice 12") in conversation
and by their **id** in URLs. `GET /invoices` returns both; find the id from
the number, never ask the user for an id.

## Confirm before you change anything

Drafting, editing and marking invoices change the user's records — and
marking one **sent** emails the client when the invoice is mirrored to
Stripe. Confirm first, every time:

1. **Drafting:** call `POST /invoices/draft` with `"dryRun": true`. The API
   resolves the client and answers with the lines it would bill, the total
   and the period; nothing is created. Show the `summary` and ask for a yes.
   On yes, call it again **without** `dryRun`, passing `client` as the
   **id** from the dry run, so it drafts exactly what was confirmed.
2. **Line edits:** say what you're about to do with the amount ("Add
   'Deposit' for $500.00 to invoice #12?") and wait for a yes. Line edits
   have no dry run; the draft's history keeps every version, so a mistake
   is visible and reversible, but still ask.
3. **Status:** "Mark invoice #12 for Northwind as paid?" For **sent**, say
   explicitly that the client will receive it if it's on Stripe.

If a dry run or a call returns 422 `ambiguous_client` or `unknown_client`,
the response lists the clients — offer them and let the user pick; never
guess. Reading (`GET`) needs no confirmation.

## Calls

### GET /invoices/unbilled
What could be invoiced, per client. `?tz=` optional.
`{ currency, clients: [{ client: { id, name }, entries: { count, hours, amountCents }, incidentals: { count, amountCents }, totalCents, unratedProjects: [{ id, name }], periodStart, periodEnd }], summary }`.
`unratedProjects` lists projects with no hourly rate: their hours aren't
priced and a draft for that client will be refused until a rate is set
(on the web). Say so.

### POST /invoices/draft
Body: `{ "client": "<id or name>", "since": "<ISO, optional>", "until": "<ISO, optional>", "tz": "<IANA>", "dryRun": true|false }`.
Without a window it bills everything unbilled for the client: completed
time entries and expenses. With `dryRun`, returns 200
`{ dryRun: true, client: { id, name }, currency, totalCents, periodStart, periodEnd, entryCount, incidentalCount, lineItems, summary }`
and changes nothing. Without it, returns 201
`{ invoice, lineItems, entryIds, incidentalIds, client, stripeMirrorError?, summary }`.
`invoice.number` is the number to tell the user; `invoice.id` is for the
line-item calls. If the client is a Stripe customer a matching draft is
created in the user's Stripe account — nothing is sent. `stripeMirrorError`
means the local draft exists but Stripe's copy didn't get made; tell the
user, they can retry from the web.
Optional header `Idempotency-Key: <any unique string>` makes a retry of
the same call return the first result instead of a second draft.

### GET /invoices/:id/line-items
`{ invoice, clientName, lineItems: [{ id, kind, description, quantityHours, unitPriceCents, amountCents, timeEntryId, incidentalId, descriptionEdited }], summary }`.
The summary numbers the lines ("1. …; 2. …") — when the user says "the
second line", that's `lineItems[1].id`. `kind` is `time` (a tracked entry),
`incidental` (an expense passed through at cost) or `custom` (written by
hand: a deposit, a fee, a discount).

### POST /invoices/:id/line-items
Adds one line to a draft. Exactly one of:
- a custom line: `{ "description": "Deposit", "amountCents": 50000 }` or
  `{ "description": "Workshop day", "quantity": 2, "unitPriceCents": 120000 }`.
  A negative `amountCents` is a discount.
- an unbilled time entry: `{ "timeEntryId": "<id>" }`
- an unbilled expense: `{ "incidentalId": "<id>" }`

Plus optional `expectedRevision` (the `invoice.revision` you last saw — a
409 `revision_conflict` means someone changed the draft in between; re-read
it) and `tz`. Returns 201 `{ invoice, lineItems, line, summary }`.

### PATCH /invoices/:id/line-items/:lineId
- Any line: `{ "description": "New text" }`. On a time or expense line,
  `"description": null` goes back to the generated text.
- Custom lines only: `{ "amountCents": 60000 }` or `{ "quantity", "unitPriceCents" }`.
  Time and expense lines are priced from the entry and the project's rate;
  the API answers 422 `amount_not_editable` — tell the user to change the
  rate or the expense, or add a custom line.
Returns `{ invoice, lineItems, line, summary }`.

### DELETE /invoices/:id/line-items/:lineId
Takes the line off the draft. A time or expense line's source goes back to
unbilled; a custom line just goes. `?expectedRevision=` optional. 422
`last_line_item` when it's the only line — void the invoice instead.
Returns `{ invoice, lineItems, removed, summary }`.

### PATCH /invoices/:id
Body: `{ "status": "sent" | "paid" | "void" }`. Draft → sent, paid or void;
sent → paid or void; paid and void are final. On a Stripe-mirrored invoice,
**sent** finalizes and emails it through Stripe; paid records an
out-of-band payment; void voids it. Voiding puts the entries back in the
unbilled pool. Returns `{ invoice, stripeSyncError?, summary }`.

### GET /invoices, GET /invoices/:id
`GET /invoices?status=draft|sent|paid|void&clientId=` →
`{ invoices: [{ id, number, status, clientName, totalCents, currency, periodStart, periodEnd, revision, sentAt, paidAt }] }`, newest first.
`GET /invoices/:id` → the full invoice with `lineItems`, `entries`,
`incidentals`, `client` and `revisions`.

Only drafts can be edited; anything further along answers 422
`invoice_not_draft`. Every edit makes a new revision and keeps the previous
one, so nothing a draft ever said is lost — and **every edit gives the lines
new ids**. Never reuse a line id from before an edit: take the ids from the
`lineItems` in the response you just got, or `GET /invoices/:id/line-items`
again before the next change.

## Errors

`{ error, message, details? }`. Notable:
- 401 — the token is wrong or revoked; ask the user for a new one.
- 402 `subscription_required` — the workspace's plan lapsed; tell the user to visit Settings → Plan & billing in Kairos.
- 403 `insufficient_scope` — the token wasn't given the Invoicing scope; the user needs to make one that has it.
- 403 `forbidden` — the user is a member, not an owner or admin, so they can't see invoices.
- 422 `ambiguous_client` / `unknown_client` — `details.clients` lists options.
- 422 `no_unbilled_entries` — nothing to bill for that client (or window).
- 422 `missing_hourly_rate` — `details.projects` lists projects without a rate; the user sets one on the web.
- 422 `invoice_not_draft`, `amount_not_editable`, `last_line_item`, `already_on_invoice` — explained above.
- 409 `revision_conflict` — re-read the draft and try again.
- 502 `stripe_unavailable` — Stripe refused; the invoice is unchanged. Relay the message.

## Example

User: "What can I bill?"
→ `GET /invoices/unbilled?tz=America/New_York`
→ relay: "Unbilled work for 2 clients: Northwind — 12.5h in 8 entries and 2 expenses, $2,115.00; Apex Medical — 4h in 3 entries, $600.00."

User: "Invoice Northwind."
→ `POST /invoices/draft {"client":"Northwind","tz":"America/New_York","dryRun":true}`
→ API: `{ client: { id: "…", name: "Northwind" }, totalCents: 211500, summary: "Would draft an invoice for Northwind: 8 entries (12.5h) and 2 expenses, $2,115.00, 2026-09-02 to 2026-09-30. Confirm to create it." }`
→ ask: "Draft an invoice for Northwind — 8 entries (12.5h) and 2 expenses, $2,115.00, Sep 2 to Sep 30?"

User: "Yes, and add a $500 deposit credit."
→ `POST /invoices/draft {"client":"<id from the dry run>","tz":"America/New_York"}`
→ relay: "Created draft Invoice #13 for Northwind: 8 entries and 2 expenses, $2,115.00."
→ ask: "Add 'Deposit credit' for −$500.00 to Invoice #13?"

User: "Yes."
→ `POST /invoices/<id>/line-items {"description":"Deposit credit","amountCents":-50000}`
→ relay: "Added "Deposit credit" (−$500.00) to Invoice #13 for Northwind — now 11 lines, $1,615.00 (v2)."
