Report completed chargeable units
POST /api/v1/usage · operationId recordCompletedUsage
For developers: your server sends this request with a server key; it is not a button in the dashboard.
Requires server-verified identity. Identical eventId/session/action/units reuse returns the original record with replayed=true, even after the browser session expired or was withdrawn (the eventId is checked before the token). New work needs a live session token, so report it within the 24-hour session life. Conflicting reuse returns 409 conflict. This records your attestation of completion; it does not independently verify work or charge money. No automatic retries.
Authentication
Send Authorization: Bearer YOUR_SERVER_KEY (siteServerKey: Site server key in Authorization: Bearer <key> (the scheme is case-insensitive and required; a bare key is 401). Shown once on creation or rotation. Publishable keys cannot authenticate). Keep the server key in server-only configuration; never send it to a browser.
Request body
Content-Type: application/json (required)
sessionToken(string, required) · minLength 1, maxLength 250: Opaque browser session token; do not log.eventId(string, required) · pattern^[A-Za-z0-9_-]{8,128}$actionId(string, required) · pattern^[A-Za-z0-9_-]{1,64}$executionId(string, optional) · pattern^[a-f0-9]{64}$: Optional stored execution ID. Must match this site, session, trusted account and action.units(integer, required) · minimum 1, maximum 1000000
{
"sessionToken": "PASTE_BROWSER_SESSION_TOKEN",
"eventId": "event_00000001",
"actionId": "export_report",
"units": 1
}Responses
- 200: Success · body
MeteredUsage - 400: bad_request: the request is invalid; message says what to fix and field names the request field · body
Error - 401: unauthorized (missing or invalid server key, missing Bearer scheme, or a malformed or unsigned session token) or session_reinit_required (a token this collector issued whose session expired, was revoked or was removed); use the code to distinguish · body
Error - 403: forbidden: the server key and session token belong to different Sites, or the call came from a browser page · body
Error - 404: not_found: unknown path, or an ID in the path that is not on this Site · body
Error - 409: conflict: the eventId was already used with different facts; do not retry, use a new eventId. identity_required: the session has no server-verified identity yet; call /v1/identify (or /api/v1/identify) for this session, then send the same request again. · body
Error - 413: payload_too_large: the request body exceeds 131072 bytes · body
Error - 429: rate_limited: wait for Retry-After · body
Error· headersRetry-After - 500: internal_error: fail the protected action; do not retry automatically · body
Error - 503: service_unavailable: temporarily unavailable; only verify retries · body
Error
Success body fields
analyticsVerdict("human" | "agent" | "unknown", optional)analyticsOnly(boolean, optional): Analytics-only: analytics counted the session as AI agent, but this usage keeps the stricter verdict (verdict) for billing.id(string, required) · pattern^meter_[a-f0-9]{64}$siteId(string, required)sessionId(string, required)userId(string | null, required) · minLength 1, maxLength 512, pattern\S: Stable authenticated ID. Maximum 512 Unicode code points for server-verified identity (browser-supplied IDs stay at 128 UTF-16 code units); whitespace is trimmed.accountId(string | null, required) · minLength 1, maxLength 512, pattern\S: Stable authenticated ID. Maximum 512 Unicode code points for server-verified identity (browser-supplied IDs stay at 128 UTF-16 code units); whitespace is trimmed.kind("work_completed", required)actionId(string, required) · pattern^[A-Za-z0-9_-]{1,64}$executionId(string, optional) · pattern^[a-f0-9]{64}$: Optional stored execution ID. Must match this site, session, trusted account and action.units(integer, required) · minimum 1, maximum 1000000source("server-reported", required)outcomeBasis("application-server-reported", optional)verdict("human" | "agent" | "unknown", required)basis(string, required)accountResponse("default" | "flagged" | "agent_seat", optional): Account response in force when the usage was recorded.via("tool" | "ui" | "unknown", optional): How the work reached your application, derived when it was recorded: tool (a reviewed tool or workflow with the same name as actionId ran in this session within 15 minutes before), ui (no tool ran in that window and UI interactions were received) or unknown (anything else; never counted as ui). Says nothing about who drove it; verdict does.observedAt(number, required): Unix epoch milliseconds.created(number, required): Unix epoch milliseconds.replayed(boolean, required)
Failures are not verdicts. Error bodies follow the Error schema: code is one of bad_request, unauthorized, session_reinit_required, forbidden, not_found, conflict, identity_required, payload_too_large, rate_limited, internal_error, service_unavailable, request_failed, approval_invalid, approval_used, approval_expired, approval_mismatch, step_up_not_held, step_up_mismatch, step_up_settled, step_up_expired, step_up_superseded, agent_tokens_disabled, tool_not_approved, tool_not_read_only. Errors and retries explains each code and what to do.
Example
/api/v1/usageServer key · run it from your backendEdit the JSON and the examples below update. Nothing is sent from this page.
curl -sS -X POST 'https://app.trytelltale.dev/api/v1/usage' \
-H "Authorization: Bearer $TELLTALE_SERVER_KEY" \
-H 'Content-Type: application/json' \
--data '{"sessionToken":"PASTE_BROWSER_SESSION_TOKEN","eventId":"event_00000001","actionId":"export_report","units":1}'const response = await fetch("https://app.trytelltale.dev/api/v1/usage", {
method: "POST",
headers: {
Authorization: 'Bearer ' + process.env.TELLTALE_SERVER_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
"sessionToken": "PASTE_BROWSER_SESSION_TOKEN",
"eventId": "event_00000001",
"actionId": "export_report",
"units": 1
}),
signal: AbortSignal.timeout(5000)
});
const result = await response.json();// server.js is the server adapter: download /adapters/v1/server.js (see Server adapters).
import { createTelltaleServer } from './lib/telltale/server.js';
const telltale = createTelltaleServer({ endpoint: "https://app.trytelltale.dev", serverKey: process.env.TELLTALE_SERVER_KEY });
const result = await telltale.recordUsage({
"sessionToken": "PASTE_BROWSER_SESSION_TOKEN",
"eventId": "event_00000001",
"actionId": "export_report",
"units": 1
});Machine-readable contract: openapi.json. Request builder: the API explorer shows every operation as curl; it sends requests only on a local dashboard.