Expenses API

Use a full-access key and send Authorization: Bearer ... and Keito-Account-Id on each request. Expenses must be enabled in the workspace. A key follows its bound identity and current project access; it does not permit creating an expense for another person. Personal read-only sync keys cannot use these endpoints.

Find a category

GET /api/v2/expense_categories

List categories with is_active, updated_since, page and per_page. Responses use the expense_categories array. Select an active category and inspect its unit_price before creating the expense. Discover visible projects with GET /api/v2/projects?is_active=true.

curl 'https://app.keito-test.com/api/v2/expense_categories?is_active=true' \
  -H "Authorization: Bearer $KEITO_API_KEY" \
  -H "Keito-Account-Id: $KEITO_ACCOUNT_ID"

Create an expense

POST /api/v2/expenses

Field Type Required Meaning
project_id string Yes Visible project in this workspace
expense_category_id string Yes Active expense category
spent_date string Yes Work date, YYYY-MM-DD
total_cost number For amount-based category Positive amount; do not also send units
units number For per-unit category Positive quantity, at most six decimal places; do not send total_cost
entry_type string No expense (default) or refund
adjustment_reason string For refunds returned_materials, vendor_refund, correction, imported_adjustment, or other
adjustment_of_id string No Original expense being refunded, subject to the refund policy
notes string No Description
billable boolean No Whether to charge the client; a nonbillable project stays nonbillable
attach_receipt_to_invoice boolean No Select the stored receipt for eligible future client invoices
source string No web, cli, api, or agent
metadata object No JSON object, at most 4 KB

The category determines whether total_cost or units is accepted. A top-level unit_price, user_id, currency or reimbursable is not a create field. The response identifies the expense’s currency; for project expenses it follows the project’s client currency, falling back to workspace currency. Future spent dates depend on the workspace setting. Submitted or approved timesheet-week locks may prevent creation.

curl --fail-with-body 'https://app.keito-test.com/api/v2/expenses' \
  -H "Authorization: Bearer $KEITO_API_KEY" \
  -H "Keito-Account-Id: $KEITO_ACCOUNT_ID" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: receipt-import-unique-001' \
  -d '{"project_id":"YOUR_PROJECT_ID","expense_category_id":"YOUR_CATEGORY_ID","spent_date":"2026-10-30","total_cost":24.50,"notes":"Imported receipt","source":"api"}'

Save the top-level id from the returned expense before uploading its receipt. Creation records bookkeeping data; it does not make a payment.

Refunds

A refund uses a positive amount or quantity with entry_type: "refund" and an adjustment_reason. Keito stores the adjustment as a negative cost. Do not send a raw negative amount. adjustment_of_id links an eligible original expense and enforces the original’s scope and remaining refundable amount; it is not permission to change billed or locked history. Refunds cannot be reclassified as ordinary expenses through an update. See the OpenAPI schemas for exact adjustment fields and limits.

List expenses

GET /api/v2/expenses

Parameter Meaning
page, per_page Standard pagination
user_id Expense owner; me selects the key’s identity. Owners/Administrators can select another person.
project_id, client_id Project/client filters
expense_category_id One category
approval_status unsubmitted, submitted, or approved
from, to Inclusive spent-date range
is_billed Billed status
invoice_id Linked Keito invoice ID
source Recorded source
updated_since ISO timestamp lower bound

Owners/Administrators receive workspace expenses when user_id is omitted; other members receive their own visible expenses. Results use an expenses array and standard pagination links.

Invoice Linkage

Expense responses include the read-only invoice_id field:

Field Type Description
invoice_id string or null The Keito invoice ID linked to the expense, or null when no Keito invoice link exists

The link covers expenses attached directly to an invoice line and expenses included in a grouped invoice line. It is a Keito invoice ID, never a Harvest invoice ID or an invoice number. Use it with the Invoices API when your credential has permission to read that invoice.

is_billed: true does not guarantee an invoice link. Expenses marked billed manually or imported as billed without a Keito invoice link return invoice_id: null. Do not infer an invoice from billed status.

To list an invoice’s linked expenses, pass its ID:

curl "https://app.keito-test.com/api/v2/expenses?invoice_id=invoice_id_here" \
  -H "Authorization: Bearer kto_xxxxx" \
  -H "Keito-Account-Id: your_company_id"

The filter combines with existing filters and pagination and only narrows expenses your credential can already read. Do not send invoice_id in POST or PATCH requests; invoice linkage is managed through the invoice workflow.

Personal read-only sync keys cannot call the Expenses API.

Get, update or delete

Method Endpoint Result
GET /api/v2/expenses/{id} Read a visible expense
PATCH /api/v2/expenses/{id} Update writable expense fields
DELETE /api/v2/expenses/{id} Delete an eligible expense and clean up its receipt

Approved, billed or locked expenses and locked timesheet weeks cannot be edited through this contract. Project/category changes must remain valid in the selected workspace. PATCH does not accept a new source or invoice association. Reimbursement classification uses the separate subresource below.

Create, update and delete support an optional Idempotency-Key. Use a unique key per operation and retry only the identical method, route and body with that key. Reconcile an uncertain response before issuing a new operation; financial locks and current authorization still apply.

Receipt files

Method Endpoint Result
POST /api/v2/expenses/{id}/receipt Upload or replace a receipt
GET /api/v2/expenses/{id}/receipt Read private file bytes
DELETE /api/v2/expenses/{id}/receipt Remove a stored receipt

Upload uses multipart/form-data with a field named file. Let the HTTP client generate the multipart boundary; do not force Content-Type: application/json. A file must be nonempty and at most 9 MiB (9,437,184 bytes). Accepted types are PDF, JPEG, PNG, GIF, WebP, HEIC, HEIF and TIFF. Upload returns 201 with receipt_file_name, receipt_file_size, receipt_content_type and receipt_url.

curl --fail-with-body 'https://app.keito-test.com/api/v2/expenses/YOUR_EXPENSE_ID/receipt' \
  -H "Authorization: Bearer $KEITO_API_KEY" \
  -H "Keito-Account-Id: $KEITO_ACCOUNT_ID" \
  -F 'file=@/path/to/receipt.pdf;type=application/pdf'

Keep the expense ID when retrying a file upload. Uploading again replaces that expense’s file; creating another expense is a separate action. GET returns the original bytes with private/no-store caching and the stored content type. Missing files return 404. Upload/removal require an editable expense and an unlocked timesheet week. A receipt cannot be uploaded to another person’s inaccessible expense.

Stored receipt presence, attach_receipt_to_invoice, and the invoice-send attachment choices are separate. Setting the expense flag does not upload a file or send it to anyone.

Read reimbursement classification

GET /api/v2/expenses/{id}/reimbursement

Read an expense’s reimbursement classification before changing it. Use a full-access API key for the selected workspace. You must be a current workspace owner or administrator, or the person who owns the expense, and retain access under the expense policy. Expenses must be enabled in the workspace. Limited sync keys and AI app connections cannot use this endpoint.

curl "https://app.keito-test.com/api/v2/expenses/expense_id_here/reimbursement" \
  -H "Authorization: Bearer kto_xxxxx" \
  -H "Keito-Account-Id: your_company_id"

The response contains only classification and its concurrency version:

{
  "expense_id": "expense_id_here",
  "reimbursable": null,
  "expense_version": 3
}
Field Type Meaning
expense_id string Expense ID
reimbursable boolean or null true: reimbursable; false: not reimbursable; null: Not classified
expense_version positive integer Current version to send when updating classification

Authorized historical classification remains readable when the reimbursement workflow is disabled. This endpoint does not return repayment amounts, payment references or reimbursement history.

Update reimbursement classification

PATCH /api/v2/expenses/{id}/reimbursement

Send the classification and the expense_version from your latest GET as expected_expense_version. Both fields are required. Explicit null restores Not classified; omitting reimbursable does not preserve its current value and returns 400.

Field Type Required Description
reimbursable boolean or null Yes true, false or explicit null
expected_expense_version positive integer Yes expense_version from the latest GET
curl -X PATCH "https://app.keito-test.com/api/v2/expenses/expense_id_here/reimbursement" \
  -H "Authorization: Bearer kto_xxxxx" \
  -H "Keito-Account-Id: your_company_id" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: expense-classification-001" \
  -d '{"reimbursable": false, "expected_expense_version": 3}'

A successful response has the same three fields as GET, with the saved classification and a new expense_version.

Writes require Pro or Business, an enabled expense reimbursement workflow, and current permission to edit the expense. The same owner, administrator or expense-owner boundary applies. Invoice, import, archive, timesheet and repayment-history protections remain in place. Classification returns an eligible expense to Unsubmitted for review; submit it again when approval is required. Client billability stays unchanged.

For a network retry, send the identical body and the same optional Idempotency-Key. A completed matching request can be replayed without classifying twice. Changed request data, changed authority or settings, or a stale expense version can return 409; read the current classification and review the conflict before starting a new operation. Disabled reimbursement writes or unavailable permissions return 403.

Use this dedicated endpoint for classification. The ordinary expense POST and PATCH payloads do not accept reimbursable, and their responses are unchanged. Classification does not record a repayment or move money; there is no repayment-recording API in this contract. Use Reimbursements and history to record repayments made elsewhere.