---
title: "Prepare an invoice for the owner's approval"
method: POST
path: "/api/v1/invoices/draft"
---

# Prepare an invoice for the owner's approval

`POST /api/v1/invoices/draft`

## Request

- Base URL: `https://tallymeter.com`
- URL: `https://tallymeter.com/api/v1/invoices/draft`
- Auth: HTTP bearer

Gets the next invoice ready – unbilled tracked time over the period plus optional manual lines and a note – and leaves it waiting on the Invoices page, where the owner reads its PDF (marked DRAFT) and approves or discards it. Nothing is numbered, issued or billed here, and no endpoint approves: only the owner issues an invoice, in the web app. One draft per workspace; preparing again replaces it. Requires the `invoices:prepare` scope and a token of the workspace owner (403 for a member).

## Request body

- object
  - `from` string, date: Period start in the owner's timezone. Omit to start where the Invoices page does (the day after the last invoice's period, earlier when unbilled time was tracked into it).
  - `to` string, date, required: Period end, inclusive; today at the latest.
  - `manual_items` object[]: Lines billed besides tracked time, e.g. costs passed through at cost.
    - `description` string, required
    - `amount` number: USD; give this or amount_eur
    - `amount_eur` number: EUR, priced at the ECB rate of the day the invoice is issued and printed as "<description> · €12.49 @ 1.1253"
  - `note` string: Shown to the owner above the Approve button.

## Response `201`

The draft is waiting for approval

- InvoiceDraft: The invoice waiting for approval as it stands now: lines and totals worked out from the tracker, and the number and dates it would be issued with today.
  - `from` string, date
  - `to` string, date
  - `invoice_number` string: the number it gets if approved now
  - `issue_date` string, date: today: an invoice is issued on the day it is approved
  - `due_date` string, date
  - `items` object[]
    - `number` integer
    - `key` string: ticket key, CALL-LOG, or MANUAL for a manual line
    - `summary` string
    - `spentTimeHours` number
    - `cost` number
  - `total_hours` number
  - `tracked_cost` number
  - `manual_cost` number
  - `total_cost` number
  - `note` string, nullable
  - `source` 'api' | 'mcp' | 'cli'
  - `prepared_at` string, date-time
  - `prepared_total_hours` number
  - `prepared_total_cost` number
  - `changed_since_prepared` boolean: the tracker moved since the draft was prepared (time logged, retitled or billed elsewhere); the lines above are as of now
  - `eur_rate` object, nullable: the ECB rate the EUR manual lines are priced at; null without any
    - `date` string, date
    - `rate` number: USD per EUR
  - `line_checks` object[]: ticket lines that would print split over several lines, under an old title, or with no ticket on the board; empty when every line matches the board. Fix by setting fix_description on each entry in entry_ids (PATCH /entries/{id}), then prepare again
    - `key` string
    - `problem` 'split' | 'retitled' | 'not_on_board'
    - `lines` string[]: the summaries the invoice would print for this key
    - `board_title` string, nullable
    - `fix_description` string, nullable: KEY board title; null when the ticket is not on the board
    - `entry_ids` integer[]
  - `fingerprint` string: SHA-256 of the lines and totals above
  - `review_url` string, uri: where the owner reads the PDF and approves or discards it

## Other responses

- `401`: Missing or invalid token
- `403`: Token lacks the required scope, or the account has no active plan
- `422`: Validation failed

---

[API](https://skmtc.dev/tallymeter/apis/tallymeter-api.md) · [All operations](https://skmtc.dev/tallymeter/apis/tallymeter-api/llms.txt) · [OpenAPI document](https://skmtc.dev/tallymeter/apis/tallymeter-api/revisions/50044367b64e?raw)
