DocumentationBrowse
Timekeeping
Not enabled on this host yet: every timekeeping call returns 503 until launch. The reference below describes the shipped surface so you can build against it ahead of time. It is free; nothing here is metered.
Signed-in lawyers do not need this reference: /time is the screen: derive the week, review the findings, approve, download the file. The API below is the same thing for firms that script it.
Every request you make through the API is already logged with a timestamp and, when you made it on a matter, the matter it belonged to. Timekeeping turns that log into time entries: it sorts your requests, splits them wherever you went idle for more than fifteen minutes, and records each run as one draft entry whose minutes are the span of the requests it observed, with a narrative stating what those requests were. You edit, approve and export.
What it captures is work done here. It does not read your calendar, your email or your documents, and a derived entry is a floor on the time you spent on a matter on this platform, never an estimate of time spent elsewhere. That is why every derived entry starts as a draft.
format=csv or format=ledes exports them.POST /time/derive
| Field | Type | Notes |
|---|---|---|
from | ISO datetime, optional | Defaults to seven days ago. |
to | ISO datetime, optional | Defaults to now. |
idle_gap_min | integer 1–240, optional | A gap longer than this starts a new session. Default 15. |
include_docket | boolean, optional | Also suggest an entry for each new filing delivered to one of your docket watches. Default true. |
docket_default_min | integer, optional | Minutes a docket suggestion starts with. Default 6. It is a prompt, not an observation; confirm it. |
{
"object": "time.derive",
"window": { "from": "2026-09-29T14:00:00.000Z", "to": "2026-10-06T14:00:00.000Z" },
"created": 3,
"requests_covered": 11,
"docket_suggestions": 1,
"data": [
{ "id": "te_8f1c2a9d4b7e6c0a1d23", "object": "time.entry", "source": "derived", "status": "draft",
"matter_id": "9b1c2e4a-…", "matter_title": "Alvarez v. Gulf Coast Rig Servs.",
"started_at": "2026-10-06T14:00:00.000Z", "ended_at": "2026-10-06T14:23:08.000Z",
"minutes": 24, "hours_billable": 0.4,
"narrative": "Alvarez v. Gulf Coast Rig Servs.: Legal research and analysis on DocketRouter, 5 research queries, 2 citation checks, 1 legal analysis.",
"activity": { "requests": 8, "request_ids": ["req_4f1e…", …], "by_kind": { "research": 5, "citation check": 2, "legal analysis": 1 } },
"docket_id": null, "created_at": "…", "updated_at": "…", "exported_at": null },
…
]
}Idempotent. A request belongs to at most one entry, so running derive over the same window again creates nothing and reports created: 0. Delete an entry and its requests are free to be derived again. Requests made on different matters are never merged into one session, even when they interleave in time.
A session starts at its first request and ends when its last request's response was observed (the request time plus its latency). A lone request is one minute, never zero. Nothing is added for time you were not making requests. Rounding to a billing increment happens at export, not here: the stored minutes are the true span.
GET /time
| Query | Default | Meaning |
|---|---|---|
from | 30 days ago | Entries whose start is at or after this. |
to | now | Entries whose start is at or before this. |
matter | any | Only entries on this matter id. |
status | any | draft, approved or exported. |
source | any | derived, manual or docket. |
format | json | csv, ledes or clio downloads a file instead. |
clio_matter, clio_user | blank | Clio only. clio_matter is required: the matter's exact Clio display number. clio_user is the timekeeper's first and last name as Clio has it. |
increment | 6 | Billing increment in minutes for hours_billable. 6 is a tenth of an hour; 15 is a quarter. |
rate | 0 | LEDES only: hourly rate. Line totals are hours × rate. With no rate the file still imports as time. |
client_id, timekeeper, timekeeper_name, law_firm_id, invoice | blank | LEDES only: the identifiers your billing system expects on each line. |
mark_exported | 0 | LEDES only: 1 sets every exported entry to exported after the file is produced. |
{
"object": "list",
"window": { "from": "…", "to": "…" },
"data": [ { "id": "te_…", "object": "time.entry", … , "hours_billable": 0.4 }, … ],
"total": 14,
"totals": { "entries": 14, "minutes": 388, "hours_billable": 6.9 },
"increment_min": 6
}CSV
date,started_at,ended_at,minutes,hours_billable,matter_id,matter_title,status,source,narrative,entry_id 2026-10-06,2026-10-06T14:00:00.000Z,2026-10-06T14:23:08.000Z,24,0.4,9b1c2e4a-…,Alvarez v. Gulf Coast Rig Servs.,draft,derived,"Alvarez v. Gulf Coast Rig Servs.: Legal research and analysis on DocketRouter, 5 research queries, 2 citation checks, 1 legal analysis.",te_8f1c…
LEDES 1998B
LEDES1998B[] INVOICE_DATE|INVOICE_NUMBER|CLIENT_ID|LAW_FIRM_MATTER_ID|INVOICE_TOTAL|BILLING_START_DATE|BILLING_END_DATE|INVOICE_DESCRIPTION|LINE_ITEM_NUMBER|EXP/FEE/INV_ADJ_TYPE|LINE_ITEM_NUMBER_OF_UNITS|LINE_ITEM_ADJUSTMENT_AMOUNT|LINE_ITEM_TOTAL|LINE_ITEM_DATE|LINE_ITEM_TASK_CODE|LINE_ITEM_EXPENSE_CODE|LINE_ITEM_ACTIVITY_CODE|TIMEKEEPER_ID|LINE_ITEM_DESCRIPTION|LAW_FIRM_ID|LINE_ITEM_UNIT_COST|TIMEKEEPER_NAME|TIMEKEEPER_CLASSIFICATION|CLIENT_MATTER_ID[] 20261006|DR-20261006|C001|9b1c2e4a-…|160.00|20260929|20261006|DocketRouter time entries|1|F|0.40|0.00|160.00|20261006|||A102|ZL|Alvarez v. Gulf Coast Rig Servs.: Legal research and analysis on DocketRouter, 5 research queries, 2 citation checks, 1 legal analysis.||400.00|||9b1c2e4a-…[]
Clio
matter,date,activity_description,note,price,quantity,type,activity_user,non_billable 00042-Okafor,10/06/2026,,"Telephone conference with client re: HOA demand letter",350.00,0.50,TimeEntry,Zach Lawless,
This is Clio Manage's own "Activities from CSV" template, header verbatim, minus the UTBMS columns Clio says to delete when a firm does not use codes. Import it at Clio → Imports → Add → Activities from CSV. Clio matches matter to its display number exactly and requires a price on every row, which is why a Clio file covers one matter at a time and clio_matter and rate are required rather than guessed. Dates are MM/DD/YYYY and quantity is decimal hours at your increment; activity_description is left blank because a non-matching value makes Clio create a new category.
LEDES 1998B is the e-billing layout most firm billing systems import. Activity codes are set from how the entry arose: A102 (research) for derived entries, A104 (review/analyze) for docket suggestions, blank for manual entries. Task codes depend on the matter phase and are left blank for you to set in your system. Pipes and line breaks are stripped from descriptions so every record stays parseable.
POST /time
| Field | Type | Notes |
|---|---|---|
started_at | ISO datetime | Required. |
ended_at | ISO datetime | Or minutes. One of the two is required. |
minutes | integer 1–1440 | Or ended_at. |
narrative | string, 1–4000 | Required. |
matter_id | string, optional | Must be one of your matters; anything else is 404. |
{ "id": "te_…", "object": "time.entry", "source": "manual", "status": "draft", "minutes": 25, "hours_billable": 0.5, … }PATCH /time/:id and DELETE /time/:id
| Field | Type | Meaning |
|---|---|---|
matter_id | string or null | Reassign to one of your matters, or unlink. |
narrative | string | Reword. |
minutes | integer | Adjust. The derived span is a floor; raise it when you did more than the requests show. |
status | draft | approved | exported | Marking exported stamps exported_at. |
started_at, ended_at | ISO datetime | Move the entry. |
At least one field is required. DELETE returns { "ok": true, "id": "te_…" } and releases the entry's requests, so a later derive can pick them up again.
A week, in four calls
# 1. derive drafts from this week's work
curl -s https://docketrouter.ai/api/v1/time/derive -H "Authorization: Bearer dr-…" -H "content-type: application/json" -d '{}' \
| jq '.data[] | {id, matter_title, minutes, narrative}'
# 2. fix one up and approve it
curl -s -X PATCH https://docketrouter.ai/api/v1/time/te_8f1c… -H "Authorization: Bearer dr-…" -H "content-type: application/json" \
-d '{ "minutes": 36, "narrative": "Research re: anti-indemnity exposure under Ins. Code ch. 151; review of Texas authority", "status": "approved" }'
# 3. add the call that happened off-platform
curl -s https://docketrouter.ai/api/v1/time -H "Authorization: Bearer dr-…" -H "content-type: application/json" \
-d '{ "matter_id": "9b1c2e4a-…", "started_at": "2026-10-06T16:00:00Z", "minutes": 20, "narrative": "Telephone conference with client re: indemnity carve-outs" }'
# 4. export approved entries as LEDES for the billing system, marking them exported
curl -s "https://docketrouter.ai/api/v1/time?status=approved&format=ledes&rate=400&client_id=C001&timekeeper=ZL&mark_exported=1" \
-H "Authorization: Bearer dr-…" -o week.ledes.txtPer-matter billing guidelines
Clients reject invoices for a short, well-known list of reasons, and the list differs by client. PUT /matters/:id/guidelines stores a client's terms on the matter as a partial override of the defaults: the billing increment, the daily hours cap, the minimum narrative length, what counts as block billing, the words that read as vague, whether unassigned time is an error. GET /time/review then lints each matter's entries under its client's set automatically; a query override applies on top for that request only. Send {} to clear.
{ "increment_min": 15, "min_narrative_words": 8, "vague_terms": ["review", "attention to", "work on"] }Clio: connect, map, push
Beyond the CSV above, approved entries can be pushed straight into Clio Manage as time entries. Three steps, once:
| Step | Call | What happens |
|---|---|---|
| Connect | GET /time/clio/connect | Returns Clio's OAuth URL, state-bound to your account for ten minutes. Clio shows the permissions the app registered (Clio has no scope parameter). On consent Clio redirects to /time/clio/callback, which stores a 30-day token and a refresh token, sealed at rest. |
| Map | PUT /time/clio/links | Clio matches on its matter id, so link each of your matters once. GET /time/clio/matters lists the firm's Clio matters to pick from. |
| Push | POST /time/clio/push | Sends approved entries as Activities of type TimeEntry, carrying date, quantity in seconds, note and matter. Already-pushed entries are skipped, so re-running is safe; pushed entries become exported. dry_run shows the exact bodies without calling Clio. |
Clio endpoints answer 503 until the host carries CLIO_CLIENT_ID, CLIO_CLIENT_SECRET and CLIO_REDIRECT_URI from a developer app registered at developers.clio.com, which needs a paid Clio login. Private apps need no Clio review. US region by default; set CLIO_REGION to eu, ca or au for a firm on another data centre (Clio's app ids are per region).
Errors and limits
| Situation | Status | Body |
|---|---|---|
| Timekeeping not enabled on this host | 503 | {"error":{"message":"timekeeping is not enabled on this host (TIMEKEEPING_ENABLED is unset)",…}} |
| No key and no session | 401 | {"error":{"message":"API key or sign-in required",…}} |
| Entry id you do not own, or that does not exist | 404 | {"error":{"message":"time entry not found",…}} |
| A matter_id that is not yours | 404 | {"error":{"message":"matter not found",…}} |
| Bad body or query | 400 | {"error":{"message":"…","type":"invalid_request_error"}} |
An entry or matter you do not own answers 404, not 403, so ids cannot be probed.
Timekeeping here covers work done on this platform. It does not integrate with Clio, PracticePanther or any other billing system beyond CSV and LEDES export, and it does not capture email, drafting in Word, calls or court time. Those are the hard parts of any timekeeper; add them manually with POST /time until they exist.
Something here wrong or missing? Mail hello@docketrouter.ai with the request_id and we will fix the docs or the API, whichever is broken.