Invoices API
All examples use:
https://app.keito.ai/api/v2
Add Authorization: Bearer <kto_...> and Keito-Account-Id: <company_id> to every API key request.
Creating, updating and sending invoices requires manager or administrator permissions. Deleting requires an administrator, and only draft invoices can be deleted. API keys inherit the permissions of the member who created them.
Create an Invoice from Tracked Time
POST /api/v2/invoices
The most common billing workflow: turn a period’s billable time and expenses into a draft invoice in a single call. Pass line_items_import and Keito builds the line items for you, applying each project’s billing rates, your workspace’s time-rounding rules, and your approval policy.
Only billable, not-yet-invoiced work is included. Everything imported is marked as billed, so the same hours cannot be invoiced twice.
This mirrors Harvest’s line_items_import, so an existing Harvest integration ports across with minimal changes.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
client_id |
string | Yes | Client ID |
line_items_import |
object | Yes | Import specification, described below |
number |
string | No | Custom invoice number. Must be unique in the workspace. |
subject |
string | No | Invoice subject |
issue_date |
string | No | YYYY-MM-DD, defaults to today |
due_date |
string | No | YYYY-MM-DD, defaults to 30 days after the issue date |
payment_term |
string | No | upon_receipt, net_15, net_30, net_45, net_60, or custom |
currency |
string | No | Three-letter code, defaults to the workspace currency |
purchase_order |
string | No | PO number |
tax |
number | No | Percentage applied to the subtotal |
tax2 |
number | No | Second tax percentage |
discount |
number | No | Discount percentage |
notes |
string | No | Notes shown on the invoice |
line_items_import
| Field | Type | Required | Description |
|---|---|---|---|
project_ids |
string[] | No | Restrict the import to these projects. Omit for all of the client’s projects. |
time |
object | No | Import tracked time |
expenses |
object | No | Import expenses |
At least one of time or expenses is required.
Each of time and expenses accepts:
| Field | Type | Required | Description |
|---|---|---|---|
summary_type |
string | Yes | How the work is grouped into line items |
from |
string | No | YYYY-MM-DD. Defaults to the earliest uninvoiced record. |
to |
string | No | YYYY-MM-DD. Defaults to the latest uninvoiced record. |
summary_type for time is project, task, people, or detailed. For expenses it is project, category, people, or detailed. detailed creates one line item per record.
Omit from and to to invoice all outstanding work regardless of date.
Example Request
curl -X POST https://app.keito.ai/api/v2/invoices \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-d '{
"client_id": "client_id_here",
"number": "MS-2026-08",
"subject": "August 2026 services",
"issue_date": "2026-08-22",
"due_date": "2026-09-21",
"line_items_import": {
"project_ids": ["project_id_here"],
"time": {
"summary_type": "task",
"from": "2026-08-01",
"to": "2026-08-21"
},
"expenses": {
"summary_type": "category",
"from": "2026-08-01",
"to": "2026-08-21"
}
}
}'
The response is the created invoice, including the generated line_items and
their IDs. Keep those IDs when the integration needs to clean up the
client-facing descriptions or rates before sending:
{
"id": "invoice_id_here",
"number": "MS-2026-08",
"state": "draft",
"line_items": [
{
"id": "line_item_id_here",
"kind": "SERVICE",
"description": "Project: Task (01/08/2026 - 21/08/2026)",
"quantity": 8,
"unit_price": 125,
"amount": 1000
}
]
}
If the import fails, no invoice is left behind — Keito removes the draft before returning the error.
Create an Invoice with Explicit Line Items
POST /api/v2/invoices
To calculate amounts yourself, pass line_items instead. line_items and line_items_import are mutually exclusive; sending both returns 400.
Line Item Fields
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | No | Item category, defaults to Service |
description |
string | No | Line description |
quantity |
number | No | Defaults to 1. Must be non-negative. |
unit_price |
number | No | Defaults to 0. Must be non-negative. |
effect |
string | No | charge or credit, defaults to charge |
taxed |
boolean | No | Apply the first tax rate |
taxed2 |
boolean | No | Apply the second tax rate |
project_id |
string | No | Attribute the line to a project |
sort_order |
number | No | Display order, defaults to array position |
Use "effect": "credit" for credit lines rather than negative numbers.
Line items supplied this way are not linked to time entries or expenses, so the underlying work is not marked as billed.
Example Request
curl -X POST https://app.keito.ai/api/v2/invoices \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-d '{
"client_id": "client_id_here",
"subject": "Consulting retainer",
"line_items": [
{
"kind": "Service",
"description": "Retainer - March",
"quantity": 1,
"unit_price": 4000,
"project_id": "project_id_here"
},
{
"kind": "Service",
"description": "Goodwill credit",
"quantity": 1,
"unit_price": 250,
"effect": "credit"
}
]
}'
List Invoices
GET /api/v2/invoices
Query Parameters
| Parameter | Type | Description |
|---|---|---|
client_id |
string | Filter by client |
state |
string | draft, open, paid, closed, or voided |
from |
string | Issue date lower bound (YYYY-MM-DD) |
to |
string | Issue date upper bound (YYYY-MM-DD) |
updated_since |
string | ISO timestamp lower bound |
page |
number | Page number, starting at 1 |
per_page |
number | Results per page, default 100, max 2000 |
Results are returned under an invoices key alongside page, per_page, total_pages, total_entries, and links.
Example Request
curl "https://app.keito.ai/api/v2/invoices?state=open&from=2026-01-01" \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id"
Get an Invoice
GET /api/v2/invoices/:id
Returns the invoice with its client, creator, and line items.
Update an Invoice
PATCH /api/v2/invoices/:id
Only draft invoices can be updated. Invoice-level fields include number,
subject, issue_date, due_date, purchase_order, tax, tax2,
discount, notes, period_start, period_end, service_period_start, and
service_period_end.
curl -X PATCH https://app.keito.ai/api/v2/invoices/invoice_id_here \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-d '{ "due_date": "2026-05-15", "notes": "Payment by bank transfer" }'
Edit Imported Line Items
Patch imported lines in place by supplying the line item id from the create
or get-invoice response and any of description, unit_price, or kind:
curl --fail-with-body -X PATCH \
https://app.keito.ai/api/v2/invoices/invoice_id_here \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-d '{
"line_items": [
{
"id": "line_item_id_here",
"description": "Consulting services",
"unit_price": 150,
"kind": "Service"
}
]
}'
Keito updates the existing line and recalculates invoice totals. The links to its imported time entries or expenses are preserved, so those source records remain billed and cannot be selected again next month.
Every object must include an ID belonging to the invoice. This endpoint does
not accept a full replacement array, line_items_attributes, a new line without
an ID, or a nested /line_items/:id route.
Download an Invoice PDF
GET /api/v2/invoices/:id/pdf
Download the invoice exactly as it is currently rendered in Keito:
curl --fail --output "MS-2026-08.pdf" \
https://app.keito.ai/api/v2/invoices/invoice_id_here/pdf \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id"
A successful response has Content-Type: application/pdf and a
Content-Disposition filename. The response is private and is not cached. The
API key must have access to the invoice’s workspace; invoices in another
workspace return 404.
Validation and Billed State
Invoice create and update requests reject unsupported fields with 400 Bad Request instead of returning a false success. The response message identifies
the unsupported field.
Billing flags are server-owned. Do not PATCH is_billed on time entries or
expenses. Create the invoice with line_items_import to mark source work as
billed; deleting the draft releases eligible source records through the invoice
workflow.
Clients can detect the new behavior in the X-Keito-Features response header:
invoice-line-item-edit,invoice-pdf
Delete an Invoice
DELETE /api/v2/invoices/:id
Administrators only, and only for draft invoices. Deleting a draft releases the time entries and expenses it captured, so they become invoiceable again.
Send an Invoice
POST /api/v2/invoices/:id/messages
Emails the invoice to its recipients and moves it from draft to open.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
event_type |
string | No | send, reminder, or thank_you. Defaults to send. |
recipients |
array | Yes | Objects with email and optional name |
cc_recipients |
array | No | Objects with email and optional name |
subject |
string | No | Email subject |
body |
string | No | Email body |
attach_pdf |
boolean | No | Attach the invoice PDF, defaults to true |
attach_expense_receipts |
boolean | No | Include expense receipts, defaults to true |
send_me_a_copy |
boolean | No | Copy the key owner |
Use reminder to chase an unpaid invoice, and thank_you once it is paid. Thank-you messages are only valid for paid invoices and do not accept CC recipients.
Example Request
curl -X POST https://app.keito.ai/api/v2/invoices/invoice_id_here/messages \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-d '{
"event_type": "send",
"recipients": [{ "name": "Accounts", "email": "accounts@example.com" }],
"subject": "Invoice for March 2026",
"body": "Please find March'"'"'s invoice attached.",
"attach_pdf": true
}'
List Invoice Messages
GET /api/v2/invoices/:id/messages
Returns the send, reminder, and thank-you history for an invoice under an invoice_messages key.
Not Yet Available
These exist in the Keito web app but are not yet exposed over the API:
- Recording and listing invoice payments
- Adding new lines to, or removing lines from, an existing invoice
- Invoice item categories
If any of these are on your critical path, get in touch — customer demand sets the order we build them in.