Find unbilled work before you invoice
list_unbilled returns billable time entries and expenses that are not attached to an invoice, grouped by client and currency with page totals, and flags time entries that have no rate. It pages at 500 rows of each kind, and billable work with no client is reported separately as unassigned rather than in a client group.
Written and reviewed by the Billr team. Published , last reviewed .
- Who this is for
- Anyone who logs work from an agent and wants to invoice the right things without re-reading a timesheet.
- Billr capabilities used
log_timelog_expenselist_unbilledupdate_time_entrycreate_invoice
What the list actually contains
list_unbilled reads two tables and returns one grouped picture: billable time entries with no invoice, and billable expenses with no invoice. Groups are keyed by client and currency, and each group carries a total_unbilled. The list pages at 500 rows of each kind, and page_totals: true is there to remind you that the totals cover the current page only.
- An entry counts as unbilled while
invoice_idis empty. Drafting an invoice attaches the source work immediately, so it leaves this list before the invoice is ever sent. - Time entries logged without a rate come back with
rate: null,amount: 0andmissing_rate: true. They are still billable work, they just have no price yet, and the invoice builder refuses to attach them until one is set. - Billable work with no client cannot be grouped into a client invoice, so it is reported in
unassigned(counts and totals by currency) and left out of the client groups instead of being silently dropped. That applies to expenses only when they were logged as billable: true, because an expense with no client is non-billable by default. - Non-billable entries never appear in either place.
Run it
01Log the work you want to invoice
This fictional example starts with an existing 0.75-hour Analytics audit entry for Harbor & Lane with no rate. The calls below add a 1.5-hour Schema markup fixes entry and a $47.50 client expense. Use the real IDs returned by your account in subsequent calls; the IDs shown here are illustrative.
Every call carries a
request_id, which is how a retry returns the original entry instead of creating a second one. Time is logged per client, with hours as a decimal and an optional rate:log_time { "request_id": "1c7f4b90-2d5a-4e83-9b16-8f0a3c5d7e21", "client_name": "Harbor & Lane", "description": "Schema markup fixes", "hours": 1.5, "rate": 150, "date": "2026-09-28" }An expense carries a request id as well. It is billable automatically only when a client is attached; without a client it is recorded as non-billable, so it never reaches this list unless you also pass
billable: true:log_expense { "request_id": "5e2b8d31-7a4c-4f09-8d6b-1a3e5c7d9f02", "client_name": "Harbor & Lane", "description": "Client lunch", "amount": 47.5, "category": "meals", "date": "2026-09-29" }02Ask what is unbilled
list_unbilled {}Call it with no other arguments to see everything the key can reach. Pass
client_idto narrow one client; because that filter is applied to both tables, a filtered call never reportsunassignedwork. When a page fills up you gettruncated: trueand anext_offsetto continue from, and the totals on that page only cover that page.03Fix anything flagged before you bill it
A missing rate is a decision, not an error, and it blocks invoicing: the invoice builder requires
unbilled, billable, priced and assignedtime. Set the rate on the entry itself and the amount is recalculated:update_time_entry { "time_entry_id": "9a2d0b71-6c34-4f52-8e19-2b7c4d0a6f31", "rate": 150 }The same tool reassigns a client or flips an entry to non-billable. It refuses entries that are already attached to an invoice, because the database keeps invoiced work immutable while that invoice exists.
04Turn the list into a draft invoice
Pass the ids from the list to
create_invoice, with noline_items: the server prices and links the source work in one step, writing one line at the exact source amount.create_invoice { "request_id": "a37e1d94-6c05-4f28-8b71-3d9a5e2c0b46", "client_name": "Harbor & Lane", "time_entry_ids": [ "3f6c1a44-5b2d-4e71-9c08-7a1d3f5b9e22", "9a2d0b71-6c34-4f52-8e19-2b7c4d0a6f31" ], "expense_ids": [ "d41b8e93-2a70-4c15-9b6f-5e8a1c3d7f08" ] }05Confirm it cleared
Ask for the list again. Anything that was billed is gone, which is the cheapest way to notice that a prompt attached the wrong entries.
Worked example
Fictional account, field names as returned by the server
You say
What is still unbilled?
Your agent calls
list_unbilled {}
Expected response, abridged (two time entries, one priced and one not, plus a client expense totaling 272.5)
{
"offset": 0,
"next_offset": null,
"page_totals": true,
"summary": "1 billing group has $272.50 unbilled (1 time entry missing a rate; confirm before invoicing)",
"clients": [
{
"client_id": "9f2a4d18-3b7c-4a6e-8f0d-1c5b2e9a7d44",
"client_name": "Harbor & Lane",
"currency": "USD",
"time": [
{
"id": "3f6c1a44-5b2d-4e71-9c08-7a1d3f5b9e22",
"date": "2026-09-28",
"description": "Schema markup fixes",
"hours": 1.5,
"rate": 150,
"amount": 225,
"missing_rate": false
},
{
"id": "9a2d0b71-6c34-4f52-8e19-2b7c4d0a6f31",
"date": "2026-09-29",
"description": "Analytics audit",
"hours": 0.75,
"rate": null,
"amount": 0,
"missing_rate": true
}
],
"expenses": [
{
"id": "d41b8e93-2a70-4c15-9b6f-5e8a1c3d7f08",
"date": "2026-09-29",
"description": "Client lunch",
"category": "meals",
"amount": 47.5
}
],
"total_unbilled": 272.5
}
]
}Drafting the invoice
After the rate is set, the ids above produce this draft. The source lines keep the server's shape: quantity 1, the exact source amount, and the tracked seconds in the description.
{
"summary": "Drafted INV-1149 for Harbor & Lane: $385.00 Source work was priced and linked atomically.",
"invoice": {
"id": "6b1f0c62-6a0f-4f38-9d0e-3a1f9a2b7c11",
"invoice_number": "INV-1149",
"status": "draft",
"currency": "USD",
"total": 385,
"balance_due": 385,
"line_items": [
{
"description": "Schema markup fixes (5400 seconds)",
"quantity": 1,
"rate": 225,
"amount": 225
},
{
"description": "Analytics audit (2700 seconds)",
"quantity": 1,
"rate": 112.5,
"amount": 112.5
},
{
"description": "Client lunch",
"quantity": 1,
"rate": 47.5,
"amount": 47.5
}
]
},
"linked_time_entry_ids": [
"3f6c1a44-5b2d-4e71-9c08-7a1d3f5b9e22",
"9a2d0b71-6c34-4f52-8e19-2b7c4d0a6f31"
],
"linked_expense_ids": [
"d41b8e93-2a70-4c15-9b6f-5e8a1c3d7f08"
],
"public_url": "https://billr.cc/i/5tRv9Md3Zh",
"pdf_url": "https://billr.cc/api/public/invoices/5tRv9Md3Zh/pdf",
"app_url": "https://billr.cc/invoices/6b1f0c62-6a0f-4f38-9d0e-3a1f9a2b7c11"
}The same call again reports the list as clear:
{
"offset": 0,
"next_offset": null,
"page_totals": true,
"summary": "No unbilled work found.",
"clients": []
}The draft is visible in the dashboard at https://billr.cc/invoices, where it can be reviewed, edited and sent. Attached work stays attached: while the invoice exists, the database keeps linked entries immutable, so there is no supported unlink step today. Review the draft before you send it.
Limitations worth knowing
- Totals are page totals.
page_totals: truesays so, andtruncatedappears when 500 rows of either kind were reached. Follownext_offsetuntil it is null before trusting a month-level number. - A missing rate contributes zero to the totals, so a group whose entries are all unpriced reports
$0.00plus a warning instead of a guess. - Work is grouped by client and currency. The server does not convert between currencies, so a group in another currency stays separate.
- Keys scoped to more than one business must pass
business_id; keys scoped to one business can leave it out. - The task-billing tools are separate and require human approval for pricing and invoicing, which is covered in Task billing.
Troubleshooting
- The list is empty but I know there is unbilled work
- Check whether the work is already on a draft invoice. Attaching happens when the invoice is created, not when it is sent, and invoiced work stays attached. Then check the billable flag: list_unbilled only returns billable entries.
- An expense I logged is missing entirely
- Expenses with no client are recorded as non-billable by default, so they are not returned by list_unbilled. Reassign it with update_expense using the client and billable true if it should be rebilled, or pass the client and billable: true when you log it next time.
- An entry shows amount 0
- It has no rate. Set one with update_time_entry and the amount is recomputed as hours times rate. create_invoice will refuse to attach a rate-less entry until then.
- The totals look low for the month
- Look for truncated: true and follow next_offset until it is null before you trust a month-level total. A page only totals what it contains.
- update_time_entry refuses my change
- The entry is already attached to an invoice, and linked work cannot be edited or unlinked while that invoice exists. Correct the draft itself with update_draft_invoice, or treat the change as a follow-up on a new document.
Sources
Reviewed 2026-09-20 against Billr's own tool implementations and the client documentation linked above.
Read next
Once the work is logged, the invoice is one prompt away. Connect an agent first if you have not already, then follow the draft-an-invoice guide for your client.
Get started