Invoices API
All examples use:
https://app.keito-test.com/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. Recording payments also requires permission to manage invoices. 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-test.com/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-test.com/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-test.com/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-test.com/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-test.com/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-test.com/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-test.com/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 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-test.com/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:
- 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.