Appearance
Token Events API
A token event records the AI token usage of one agent session, for one model, on one issue. kendo adds every event to the issue's token_total, and the sprint and epic totals add up their issues. Send the numbers your agent's harness measured. kendo stores counts only, never prompts or responses.
For Claude Code, the Token Usage guide has a ready hook that sends every session through the CLI. AI assistants on the kendo MCP server can use the record-token-usage tool instead.
Permissions
Ingestion takes a token with the write scope:
| Authentication | Required | Scope |
|---|---|---|
| Personal access token or CLI login | The write scope | Projects the token's user can access |
| Project token | Read & write | The single project the token is linked to |
The token's user (for a project token, its creator) must also hold the Token Events: Create permission in the project. The Member role and every role that existed on 2026-10-08 hold it. A role created later starts with every Create permission off, this one included, and an admin turns it on at the Roles page (/roles). Admins and the project owner always pass.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
POST | /api/projects/{projectId}/token-events | Record one event or a batch of up to 100 |
Record Token Usage
POST /api/projects/{projectId}/token-events
The endpoint returns 202 Accepted with an empty body.
bash
curl -X POST https://{tenant}.kendo.dev/api/projects/1/token-events \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..." \
-H "Content-Type: application/json" \
-d '{
"issue_key": "KD-0042",
"model": "claude-opus-5-5",
"tokens_in": 1200,
"tokens_out": 350,
"tokens_cache_read": 40000,
"tokens_cache_creation": 6000,
"source": "api",
"occurred_at": "2026-10-08T14:00:00Z",
"external_event_id": "6f1c2b9e-0d4a-4c1e-9a7b-2f3e8d5c1a90:claude-opus-5-5"
}'text
(empty body)To send several events in one request, wrap them in an events array: {"events": [{...}, {...}]}. A batch is all or nothing: if one event fails, kendo stores none of them.
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
issue_id | integer | One of | The numeric ID of the issue (send issue_id or issue_key, not both) |
issue_key | string | One of | The issue key exactly as kendo shows it, e.g. "KD-0042", max 20 characters |
model | string | Yes | The model the tokens were spent on, max 255 characters (e.g. claude-opus-5-5) |
tokens_in | integer | Yes | Input tokens, not counting cache reads or cache writes, at least 0 |
tokens_out | integer | Yes | Output tokens, at least 0 |
tokens_cache_read | integer | null | Yes | Tokens read from the prompt cache. The key must be present: send null when your harness does not report it, which kendo stores as "not reported" rather than 0 |
tokens_cache_creation | integer | null | Yes | Tokens written to the prompt cache. Same rule as tokens_cache_read |
source | string | Yes | How the event was captured: api for your own integration, cli for the kendo CLI. ally_hook and backfill are also accepted. mcp is set only by the MCP tool, and hand_to_claude is retired |
occurred_at | string | Yes | When the usage happened, ISO 8601 (e.g. 2026-10-08T14:00:00Z), max 255 characters |
external_event_id | string | No | Your idempotency id, max 255 characters. See Resending a session |
kendo ignores any other field.
Resending a Session
Send one event per session and model, with an external_event_id that names both, such as <session id>:<model>. When kendo already holds an event with that id:
- Same issue, same or later
occurred_at: kendo replaces the stored totals with the new ones. A retry stores nothing twice, and a resumed session's larger total replaces its earlier one. - Same issue, earlier
occurred_at: kendo keeps the stored totals, so a late retry never lowers them. - Another issue: kendo ignores the event. An id stays on the issue it was first sent for.
The id is unique across the whole workspace, not per project or per user. Use an id nobody else can choose by accident, such as a session UUID. An event without an external_event_id is always stored as a new row, so a retry counts it twice.
How the Total Is Counted
An issue's token_total is the sum of tokens_in, tokens_out, tokens_cache_read and tokens_cache_creation over its events, with a null counted as 0. The Issues API returns it on each issue.
Common Errors
| Status | Cause |
|---|---|
401 | Token is missing, invalid, revoked, or expired |
403 | Token lacks the write scope, a project token is not linked to the target project, or the token's user lacks Token Events: Create |
404 | An issue_id or issue_key names no issue in this project. Nothing in the batch is stored |
422 | Request validation failed, including an event that sends both issue_id and issue_key, or neither |
429 | Rate limit exceeded: 60 requests per minute per token |
See Also
- Token Usage guide — Send every Claude Code session automatically
- MCP Token Usage tool — Record usage from an AI assistant
- Project Tokens API — Create a Read & write token
- Tokens API — Personal access tokens