Developers
The ingest API
The same HTTP API the official plugins use. Write a plugin for an editor we do not support, or send time from a script. Field names follow WakaTime's snake_case. An editor that already has a WakaTime plugin needs no code at all: see WakaTime plugins.
Every path is on https://inlinr.com. Hover a field name in an example to read what it means.
On this page
1. Get a device token
Each editor on each machine is its own device, with its own token, revocable from Settings. There are no account-wide API keys. Start a device sign-in, show the user the link, and poll until they approve it with GitHub:
POST/api/auth/device
Content-Type: application/json { : "my-plugin", : "vscode", : "linux" }
200
{ : "…", : "ABCD-1234", : "https://inlinr.com/activate", : "https://inlinr.com/activate?code=ABCD-1234", : 900, : 5 }
POST/api/auth/device/token
Content-Type: application/json { : "…" }
400Not approved yet. Poll again after interval seconds.
{ : "authorization_pending" }
400The code expired. Start again.
{ : "expired_token" }
200Approved. Keep the token.
{ : "in_d_…", : { … }, : { … } }
Send the token as Authorization: Bearer in_d_… on every call below. POST /api/auth/device/revoke with the same header signs the device out.
2. Send heartbeats
A heartbeat says “this file was active at this time”. Send one about every two minutes of activity per file, plus one on each save. Durations are computed on the server from consecutive beats, so never send a duration. Up to 1000 beats per request.
POST/api/v1/heartbeats
Authorization: Bearer Content-Type: application/json [ { : "src/routes/index.tsx", : 1734523920.123, : "git@github.com:you/app.git", : "file", : "coding", : "typescript", : "main", : false, : 42, : 1023, : 180, : 14, : 2, : "copilot", : 12, : 1, : "vscode", : "1.99.3", : "my-plugin/0.1.0" } ]
200
{ : [[{ : "hb_0" }, 201]], : 1 }
Heartbeat fields
- entityRequired
- File path relative to the repository, a URL or an app name.
- timeRequired
- Unix seconds; fractions are kept.
- project_git_remoteRequired
- The repository's remote. The project is created or found from it.
- type
- file (default), app or domain.
- category
- coding, debugging, building, code-reviewing, writing-tests or ai-coding.
- is_write
- true when the beat comes from a save. Defaults to false.
- lines_addedlines_deleted
- Lines changed since the previous beat. 0 means none; leave the field out if you do not count.
- ai_tool
- copilot, cursor, claude-code, codeium, windsurf or aider. Any other value is rejected. Send it only when you saw the assistant edit.
- ai_lines_addedai_lines_deleted
- The part of the line counts the assistant wrote.
- ai_sessionai_model
- One assistant conversation, and the model it used.
- ai_input_tokensai_output_tokensai_cache_read_tokensai_cache_write_tokens
- Token counts. Cache tokens are separate because they bill at different rates.
- editoreditor_version
- The host editor, e.g. vscode, cursor, windsurf; and its version.
- plugin
- Your client, user-agent style: name/version.
- branchlanguagelinenocursorposlines
- Optional context. lines is the file's total line count.
Status codes
- 200Stored. The
responsesarray has one entry per beat, in order, so you can drop exactly what was accepted from your queue. - 400Malformed. Do not retry the same payload.
- 401The token is missing or revoked. Ask the user to sign in again.
- 5xxOr a network error: keep the beats and retry later.
3. Commit line counts (optional)
Counts only, read from git log --numstat on the machine: no commit message, no author email. Up to 1000 commits per request.
POST/api/v1/commits
Authorization: Bearer Content-Type: application/json { : "git@github.com:you/app.git", : [ { : "9f2c1ab", : "2026-10-14T09:12:00Z", : 3, : 120, : 14 } ] }
200New commits stored, and commits already known with no line counts filled in.
{ : 1, : 0 }
200No heartbeat has created this project yet. Not worth retrying.
{ : 0, : "unknown project" }
4. Your time
For a status bar clock: time tracked today in the user's own time zone, with the same counting rule as the dashboard.
GET/api/v1/me/today
Authorization: Bearer
200
{ : 12840, : 214, : [ … ], : [ … ] }
A period's totals, by project and language, and the time, lines and cost of AI assistants, as the AI usage page shows them. period takes the dashboard's presets (today, 7d, month…) or custom with from and to; project takes a project name or its git remote.
GET/api/v1/me/summary?period=7d&project=acme-app
Authorization: Bearer
200400 ambiguous_project when two of your projects share the name; use the git remote.
{ "period": "Last 7 days", "since": "2026-10-09T00:00:00.000Z", "until": null, "project": "acme-app", "total_seconds": 44940, "by_project": [{ : "acme-app", : 44940, "ai_seconds": 9120 }], "by_language": [{ : "typescript", : 38100 }], "ai": { : 9120, "sessions": 14, : 1820, "total_lines_added": 2410, "tokens": { … }, "cost_usd": 18.42, "unpriced_tokens": 0, "prices_verified_on": "2026-10-01", "by_tool": [ … ] } }
Logs a manual entry. Only minutes is required (1 to 1440). type is one of meeting, call, review, doc, research, testing, email or custom (the default), and started_at defaults to that many minutes ago.
POST/api/v1/me/entries
Authorization: Bearer Content-Type: application/json { "minutes": 45, : "meeting", "label": "Kick-off", "billable": true, "project": "acme-app" }
201400 invalid_entry names the field that is wrong.
{ : "cm…", : "meeting", "label": "Kick-off", "started_at": "2026-10-15T09:15:00.000Z", "minutes": 45, "billable": true, "project": "acme-app" }
5. Package releases
What the public package pages say, as JSON, for a script, a bot or an AI agent: an npm package's releases with their notes and advisories, what changes between two of its versions, and a check of a whole package.json. No token needed. Scoped names go in the path as they are: /api/v1/packages/@tanstack/react-router/releases.
GET/api/v1/packages/react/releases?limit=1
No body.200
{ : "react", : "19.2.0", : null, "license": "MIT", "repository_url": "https://github.com/facebook/react", "homepage": "https://react.dev/", : "2026-10-15T08:00:00.000Z", : false, : [ { "version": "19.2.0", : "2026-10-01T16:00:00.000Z", : false, : "## 19.2.0 …", : "https://github.com/facebook/react/releases/tag/v19.2.0" } ], : [] }
limit goes up to 100, since takes an ISO date, and prereleases=1 keeps release candidates. Leave to out of an upgrade to go to the latest release:
GET/api/v1/packages/react/upgrade?from=18.3.1&to=19.0.0
No body.200
{ : "react", : "19.2.0", : "2026-10-15T08:00:00.000Z", : false, "from": "18.3.1", "to": "19.0.0", : 1, : 1, : [], : [{ "version": "19.0.0", "markdown": "Remove propTypes …" }], "deprecations": [], : [], : [], "security": [], "features": [{ "version": "19.0.0", "markdown": "Actions …" }], "fixes": [], "other": [], "internal_lines": 0, : [{ "label": "React 19 upgrade guide", "url": "https://react.dev/…" }] }
404
{ : "not_found", "message": "No published release 19.9.9 after 18.3.1." }
POST/api/v1/check
Content-Type: application/json { : ["react@^18.2.0", "request"] }
200With a device token, each dependency also lists your_projects.
{ "summary": { "checked": 2, "with_advisories": 1, : 1, "majors_behind": 1, "behind": 1, "unchecked": 0 }, "truncated": 0, : [ { : "request", "range": null, "kind": "prod", "resolved_name": null, : "checked", "unchecked_reason": null, "latest": "2.88.2", : null, : null, : "request has been deprecated …", : [{ : "GHSA-p8p7-x288-28g6", "summary": "…", "severity": "moderate", "url": "…", : null }], "advisories_unknown": false } ] }
Without a token, an address gets 60 reads and 10 checks every ten minutes. A device token raises that to 600 and 60, and each answer then lists your_projects: your own followed projects that declare the package, with the range and the installed version. Past the limit the answer is 429 with Retry-After. 503 busy means npm could not be read for a package we had never seen; try again in a minute. A range we cannot read, like workspace:*, is reported as unchecked, never as up to date.
WakaTime plugins
Any stock WakaTime plugin can send time here, unchanged. Create a WakaTime key in Settings → Editors (it starts with waka_), then point the plugin at Inlinr in ~/.wakatime.cfg:
[settings] api_url = https://inlinr.com/api/compat/wakatime/v1 api_key = waka_…
The key travels as HTTP Basic auth, the way wakatime-cli sends it. Projects are matched by name, since WakaTime plugins do not send a git remote, and the plugin's status bar shows today's time from Inlinr. Setup for each editor is on the WakaTime integration page. Inlinr is not affiliated with or endorsed by WakaTime.
Before you write a client
The Inlinr CLI already does sign-in, an offline queue and retries. Most plugins only spawn inlinr heartbeat with flags, which is how the VS Code extension works. How time is computed from beats is in this guide, and what we store is on the security page.