Reference
The write API
One endpoint, for one job: handing TickTrac work that is already finished. An agent or a script files its own time, so you do not retype it. The rows it writes are ordinary time entries. They sit under the client, they are marked as written by a machine, and they count in the export you bill from.
Authentication
Every call carries a bearer token in the Authorization header. Create one in Settings → API tokens and give it a label. Copy it straight away. TickTrac keeps only a one-way hash and the first eight characters, so nobody can see the token a second time. A revoked token stays in the list as a record.
Authorization: Bearer 0f3c9ab1… # 64 hexadecimal characters
A token is full or agent-only, chosen when you create it. A full token writes any entry, yours included. It is for your own scripts and exporters. Every token made before the choice existed is a full one. An agent-only token files agent time and nothing else. An entry it sends without a source is an agent entry. One that asks for "source": "human" is refused. Give an agent the agent-only kind, so it cannot write hours as you.
A token belongs to one account and writes only into it, so there is no user id to send. A header that is missing, malformed or revoked answers 401 with a JSON body saying which of the three it was:
{
"error": "unauthorized",
"message": "That token is not valid. It was revoked, or it never existed."
}
POST /api/v1/entries
The body is JSON shaped {"entries": [ … ]}, with up to 100 entries in one call. Anything else is a 400 and writes nothing: a bare array, a missing key or broken JSON.
curl -X POST https://ticktrac.com/api/v1/entries \
-H "Authorization: Bearer $TICKTRAC_TOKEN" \
-H "Content-Type: application/json" \
-d '{"entries": [{
"ref": "acme-142",
"name": "Refactor the export filter",
"client": "Acme",
"categories": ["Development"],
"started_at": "2026-10-25T09:12:00Z",
"ended_at": "2026-10-25T11:42:00Z",
"tags": ["Claude"],
"source": "agent",
"agent": "claude-code",
"human_equivalent_hours": 6
}]}'
Calls are rate limited per token: 120 in a sliding 1 hour. Over it you get a 429 with a Retry-After header. Nothing of that batch was written. Wait for the window, then send the same batch again.
The fields
| Field | Required | What it is |
|---|---|---|
| ref | yes | Your own identifier for this entry, such as a task id. Up to 128 characters, and unique within your account. It makes the call safe to repeat (see below). |
| name | yes | What the work was. Up to 255 characters. An empty name is refused. In the app an entry may stay unnamed, but over the API you say what you did. |
| client | no | Who the work is for. A new name is created, so nothing needs setting up first. The export filters on it. |
| categories | no | A list of names, e.g. ["Development"]. New names are created. A category's agent ratio applies to the row (a tag's ratio comes first). |
| category | no | The older form, still accepted: a path, Client : Category : Category. The first part is the client and the rest are categories. If client or categories is sent as well, that field wins. |
| started_at | yes | ISO-8601 with an explicit offset, e.g. 2026-10-25T09:12:00Z. A timestamp without one is refused, not guessed at. |
| ended_at | yes | Same format. The entry is already finished. The API cannot start a running timer. |
| tags | no | An array of strings, each up to 255 characters. Tags that do not exist yet are created. A comma inside a tag becomes a space, since commas separate them internally. |
| source | no | agent or human, defaulting to human on a full token and to agent on an agent-only one, which refuses human. Anything else is refused. Send agent for work a machine did. Every screen filters on it. |
| agent | no | The agent's own name, up to 64 characters, e.g. claude-code. Shown on the entry's row. Agent entries only. |
| human_equivalent_hours | no | The agent's estimate of how long a person would have needed, in decimal hours (3.5 = 3:30), above 0 and up to 1000. Information only: the entry bills its true time, from started_at to ended_at. A ratio set in Settings replaces it on screen and in the export. The sent value stays stored. Agent entries only. |
Sending the same batch twice
A repeated ref is a no-op, not a conflict. The entry that already exists is returned with "created": false and its id, and nothing in it is overwritten. A job whose cursor rewinds, or a retry after a timeout, never makes a duplicate row. So a caller can resend when in doubt.
To change an entry, edit it in the app. The API writes. It does not update.
The limits, and what they refuse
- 24 hours is the longest one entry may span. A longer one is more likely a bug than a long day, so it is refused. Split it, or check the timestamps.
- Nothing in the future. Both times must be at or before now, in UTC.
- ended_at may not precede started_at.
- 100 entries per call, and 120 calls per 1 hour per token. Both stop a looping client from flooding your billing data.
What comes back
One bad entry never rejects the batch. The good rows are still written. Each item lands in results or in errors. The status code tells you which, without reading the body:
| Status | Means |
|---|---|
| 200 | Every entry was accepted. |
| 207 | Some were accepted, some were not. Read errors. |
| 422 | The body parsed, and not one entry was accepted. |
| 400 | The body is not shaped {"entries": [ … ]}, or the batch is empty or over the cap. Nothing was written. |
| 401 | No token, a malformed header, or a revoked token. |
| 429 | Over the request rate. Retry-After says when, and re-posting is safe. |
{
"results": [
{"ref": "acme-142", "id": 5312, "created": true},
{"ref": "acme-143", "id": 5290, "created": false}
],
"errors": [
{"index": 2, "ref": "acme-144",
"message": "That entry spans more than 24 hours. Split it, or check the timestamps."}
]
}
Every message says what happened and what to do about it.
An agent entry in the export
A row written through this endpoint with source: agent behaves like any other row in a client export. Three things set it apart from your own:
- It counts. Its true time goes into the total, the billing units at your increment, and the amount at your rate. It bills hour for hour, like time you tracked by hand. The human-equivalent is shown beside it, for information only.
- It is labelled. An Agent badge on the row in the export preview, plus any tags the caller sent. The downloaded CSV, JSON or TXT has the same columns for every row: Units, Amount (true) and Amount (units). A Source column tells them apart: manual for a row you tracked, agent (name) for one written here.
- It can be left out. Tick Leave out agent work on the export screen, or add noagents=1 to the URL. The file then holds only your own hours.
Home and Stats work the other way round. Agent entries are hidden until you turn on Show agent entries in Settings. They are never suggested when you start a timer.
Connect an agent (MCP)
An agent client can file its own time with no code from you. TickTrac is a remote MCP server at https://ticktrac.com/api/mcp (Streamable HTTP). Create an agent-only token in Settings. It comes with this command filled in. For Claude Code, run it once:
claude mcp add --transport http --scope user ticktrac https://ticktrac.com/api/mcp \
--header "Authorization: Bearer <agent-only token>"
Cursor, VS Code and other clients take the same URL and header. The server offers four tools. With start_session and finish_session, TickTrac measures the time itself. log_session files work with known times. list_labels lets the agent reuse your client and category names. Every entry these four file is an agent entry, whatever token it holds. Sessions keep nothing on the server, so several agents can run at once. A session over 24 hours cannot be finished as one entry. Tool calls share the 120-per-1 hour budget above.
claude.ai, Claude Desktop and ChatGPT
These connect with a sign-in rather than a pasted token. Add a custom connector with the URL https://ticktrac.com/api/mcp. The app sends you to TickTrac. You sign in and choose Allow, and it is connected. A connected app files its own work as agent time. It can reach the MCP server and nothing else. It appears under Settings → Connected apps, where you can revoke it.
For client authors: TickTrac is an OAuth 2.1 authorization server with authorization code + PKCE (S256 only), dynamic client registration, and two scopes: agent, always granted, and time, granted only when the account owner ticks it on the consent screen, whatever the client asked for. The MCP server's 401 points at its protected resource metadata (https://ticktrac.com/.well-known/oauth-protected-resource/api/mcp); the server metadata is at https://ticktrac.com/.well-known/oauth-authorization-server. Access tokens last 60 minutes and are bound to the MCP URL. Refresh tokens work once and are replaced on every refresh.
Your own time, from a chat
A connected app can also log your own hours when you tell it to. Tick Also log and run my own time when I ask on the consent screen. It is off unless you tick it. An app you connected before this existed has it off; to switch it on, revoke the app in Settings and connect it again. Settings shows which apps have it. A full token from Settings has it too. An agent-only token never does.
With it, the server offers three more tools. Each one writes a row as yours, not as agent time:
- log_my_time: a finished stretch of work. Send minutes (ending now, or at ended_at), or started_at and ended_at. It has the same limits as POST /api/v1/entries.
- start_my_timer: a running timer, as if you started it in the app. Other running timers keep running.
- stop_my_timer: stops a timer by id, or the one that is running. If several are running, it asks which one.
Say “file an hour of work for Acme” and it calls log_my_time with minutes: 60 and client: "Acme". An unknown client or category is created, as it is from the API. The assistant is told to use these tools only when you ask, never for its own work.
A custom GPT in ChatGPT
A custom GPT can file time through this API as an Action. The OpenAPI 3.1 schema is at https://ticktrac.com/docs/api/openapi.json, built from the same limits this page shows. In the GPT editor:
- Configure → Create new action → Import from URL, and paste the schema URL.
- Authentication → API Key, auth type Bearer, and paste a token from Settings.
- Set the privacy policy to https://ticktrac.com/privacy.
- Tell it in the instructions to send times with your timezone offset and a ref per entry.
Use a full token if the GPT logs your own hours as you dictate them. Use an agent-only token if it only files work it did itself.
Getting started
Create an account (two fields), then Settings → API tokens → Create token. Post one entry with a ref you can recognise. Then post it again. The second answer should say "created": false and give the same id. That tests the whole integration.