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 the relevant manager or administrator permissions. Recording payments also requires permission to manage invoices. Deleting requires an administrator, and only draft invoices can be deleted. An API key acts with the current permissions of the identity it is bound to: personal keys act as their creator, while administrators can issue integration keys bound to another identity. The key never inherits extra permissions from an administrator who issued it. See Authentication.
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 | Requested final invoice number. Keito holds it separately while the invoice is a draft, revalidates it when the invoice is issued, and then makes it the invoice’s number. It must be unique in the workspace at issue time. |
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. A new draft’s number is a provisional DRAFT-<UUID> reference,
even when the request included a requested final number. Keito assigns the
final sequential or requested number only when the draft is issued. Do not
export the provisional value to an accounting system as the final invoice
number.
Keep the line-item IDs when the integration needs to clean up the client-facing descriptions or rates before sending:
{
"id": "invoice_id_here",
"number": "DRAFT-123e4567-e89b-12d3-a456-426614174000",
"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"
Scheduled Accounting Export
An accounting integration can poll this endpoint on a schedule using a rolling
from and to issue-date window and an invoice state. The state
parameter accepts one value per request; poll separate states when the
integration needs more than one. Follow the response’s pagination links until
links.next is null.
Each listed invoice includes the same invoice and line-item fields returned by the item endpoint:
| Field | Description |
|---|---|
id |
Stable Keito invoice ID |
client_id |
Client ID for the related client lookup |
client |
Embedded client ID and name |
number |
Invoice number |
state |
Current invoice state |
issue_date |
Invoice issue date |
due_date |
Invoice due date |
currency |
Three-letter currency code |
amount |
Total invoice amount |
due_amount |
Remaining unpaid amount |
line_items |
Ordered invoice lines |
updated_at |
Last update timestamp |
Each entry in line_items includes id, kind, description,
quantity, unit_price, amount, effect, taxed, taxed2,
project_id, and sort_order. The kind value is the invoice item
category used on that line and can be mapped directly to an accounting-system
article or category.
Use GET /api/v2/clients/:client_id when the destination also needs client
details. The response includes id, name, address, currency,
payment terms, tax settings, and discount. address is a single free-form
postal-address field rather than separate street, city, and postal-code fields.
Get an Invoice
GET /api/v2/invoices/:id
Returns the invoice with its client, creator, and line items.
Record an Invoice Payment
POST /api/v2/invoices/:id/payments
Records a partial or full payment against a non-draft invoice. A payment equal
to the remaining due_amount marks an open invoice as paid; a smaller payment
leaves it open. Payments must be positive and cannot exceed the amount due.
This endpoint requires a manager or administrator with permission to manage invoices.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
amount |
number | Yes | Payment amount, up to the invoice’s current due_amount |
paid_at |
string | No | ISO 8601 timestamp with an offset; defaults to the current time |
notes |
string | No | Optional payment notes |
transaction_id |
string | No | External payment or transaction reference |
payment_gateway |
string | No | Name of the originating system, such as Fortnox |
send_thank_you |
boolean | No | Overrides the workspace’s automatic thank-you setting |
Use an Idempotency-Key header when importing a payment so a retried request
cannot record the same payment twice. Set send_thank_you to false when
the connector should never send customer email.
curl --fail-with-body -X POST \
https://app.keito.ai/api/v2/invoices/invoice_id_here/payments \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: fortnox-payment-FTX-2026-000123" \
-d '{
"amount": 1250,
"paid_at": "2026-09-06T10:30:00+02:00",
"transaction_id": "FTX-2026-000123",
"payment_gateway": "Fortnox",
"send_thank_you": false
}'
A successful request returns 201 Created with the recorded payment,
including its id, invoice_id, amount, payment dates, external
reference, payment gateway, and the resulting accounting-sync status.
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-payments,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 or mark an invoice as sent
POST /api/v2/invoices/:id/messages
Use this endpoint to send an invoice email, issue a draft without emailing anyone, resend an issued invoice, send a reminder, or send a payment thank-you.
For an integration that delivers the invoice through another system, use
event_type: "mark_sent". This atomically assigns the draft’s final sequential
or requested number, moves it from draft to open, records it as sent
externally, and does not send an email. It follows the same issue path as a
normal send, including configured Xero auto-sync.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
event_type |
string | No | send, mark_sent, resend, reminder, or thank_you. Defaults to send. |
recipients |
array | Conditional | Objects with email and optional name. Recipients are required only for email events; omit them for mark_sent. |
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 |
include_attachments |
boolean | No | Include uploaded invoice files and generated time reports, defaults to true |
send_me_a_copy |
boolean | No | Copy the identity the key is bound to |
confirm |
object | No | The invoice amount and three-letter currency the integration expects; optionally include the distinct recipient_count. A mismatch returns 409 before the invoice changes. Use recipient_count: 0 for mark_sent. |
Use send for the first emailed delivery, resend for another copy of an open
or paid invoice, reminder to chase an outstanding invoice, and thank_you
once it is paid. Thank-you messages do not accept CC recipients.
Use an Idempotency-Key header for every issuing integration request. The key
must contain 8–255 letters, numbers, dots, underscores, or hyphens. An identical
retry with the same key replays the stored response for 24 hours; reusing the
key with a different request returns 409. If a request times out or returns an
unresolved idempotency error, read the invoice before starting a new attempt.
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
}'
Mark as sent without email
This is the integration flow for assigning the final number before exporting the invoice to another system:
curl --fail-with-body -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" \
-H "Idempotency-Key: issue-invoice-invoice_id_here" \
-d '{
"event_type": "mark_sent",
"confirm": {
"amount": 1000,
"currency": "AUD",
"recipient_count": 0
}
}'
The POST response is the new invoice-message record, not a refreshed invoice. After it succeeds, fetch the invoice and export only the returned final number:
curl --fail \
https://app.keito.ai/api/v2/invoices/invoice_id_here \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id"
Confirm that the response has state: "open" and that number is the final
sequential or requested number rather than a provisional DRAFT-… reference.
If the POST outcome was uncertain, perform this GET before retrying or exporting.
List Invoice Messages
GET /api/v2/invoices/:id/messages
Returns the send, resend, external-delivery, reminder, and thank-you history for
an invoice under an invoice_messages key. A successful mark_sent action is
recorded as a send event with no recipients.
Not Yet Available
These exist in the Keito web app but are not yet exposed over the API:
- Listing invoice payments
- Adding new lines to, or removing lines from, an existing invoice
- Listing the workspace’s invoice item categories
If any of these are on your critical path, get in touch — customer demand sets the order we build them in.