Record a single agent task. Returns the stored event with computed hours_saved, cost_saved, and net_saved.
Auth
Authorization: Bearer hh_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Request body
{
agent_id: string, // required, your stable id
task_type: string, // required, see /docs/api/task-types
outcome: "success" | "failure" | "needs_review",
agent_duration_seconds?: number, // wall-clock seconds the agent took
agent_cost?: number, // cost of the agent run, in org currency
model?: string, // model id; ideally the OpenRouter id, e.g. "anthropic/claude-opus-4.8"
tokens_in?: number, // input/prompt tokens for this run
tokens_out?: number, // output/completion tokens for this run
human_baseline_minutes?: number, // overrides task_type default for this event only
metadata?: object, // free-form, indexed for /v1/reports
audit_sample?: {
input_excerpt?: string, // up to 2000 chars
output_excerpt?: string, // up to 2000 chars
model?: string,
tokens_in?: number,
tokens_out?: number,
},
occurred_at?: ISO 8601, // defaults to receipt time
}Headers
| Header | Required | Notes |
|---|---|---|
Authorization |
yes | Bearer hh_* |
Content-Type |
yes | application/json |
Idempotency-Key |
optional | Unique-per-attempt string (1–255 chars). Use a fresh value for every logical run; reuse it only when retrying the exact same run. See the Idempotency section below. If you do not have a stable per-run id, omit the header and every request creates a new event. |
Response (201)
{
"event_id": "evt_01HXY...",
"agent_id": "support-classifier",
"task_type": "email_classification",
"outcome": "success",
"resolved_baseline_minutes": 4.0,
"resolved_baseline_source": "builtin",
"resolved_hourly_rate": 45,
"currency": "EUR",
"agent_duration_seconds": 4,
"hours_saved": 0.066,
"cost_saved": 2.97,
"agent_cost": null,
"resolved_cost_source": "none",
"net_saved": 2.97,
"occurred_at": "2026-05-06T10:42:00.000Z"
}resolved_baseline_source is one of request | override | builtin | custom | fallback and tells you which layer of the resolution stack supplied the baseline.
Run cost and net ROI
net_saved is cost_saved - agent_cost. Pass agent_cost directly, or pass model plus tokens_in / tokens_out and HumanHours computes the run cost from the model's price and subtracts it. The response field resolved_cost_source is provided, computed, or none.
See Model cost and net ROI for the model id to use, the auto-updating price book, and per-model reporting.
Errors
| Code | HTTP | When |
|---|---|---|
missing_authorization |
401 | No Authorization header |
invalid_api_key |
401 | Token format invalid or unrecognized |
key_revoked |
401 | Key exists but was revoked |
validation_error |
422 | Body fails schema validation |
unknown_task_type |
422 | task_type not in catalogue and no inline human_baseline_minutes |
occurred_at_out_of_range |
422 | More than 90 days old or > 24h in the future |
rate_limited |
429 | Per-key per-second cap exceeded; retry after Retry-After seconds |
quota_exceeded |
429 | Free plan monthly cap reached |
See errors for the full catalogue.
Idempotency
Send the same Idempotency-Key twice within 24 hours and you get the same event back instead of a new one. The key is scoped to your org (not the API key), so retries from a different API key still dedupe correctly.
This is meant for safe retries of one logical run, not for deduplication across runs.
Use a value that is unique per agent run. Each fresh run gets its own key; retries of the same run reuse the key.
| Source | Example |
|---|---|
| UUID generated per call | crypto.randomUUID() (Node, browsers) or uuid.uuid4() (Python) |
| Workflow execution id | n8n: {{ $execution.id }}. Make: {{1.bundle_id}}. Zapier: zap_meta_human_id. |
| Inbound message id | The id of the email, ticket, or queue message that triggered the agent |
| Request id | A correlation id from your own logger / tracer |
Do not use values that repeat across runs, or every subsequent call within 24h is silently deduped to the first event:
| Anti-pattern | Why it breaks |
|---|---|
agent_id |
One event per agent per 24h, every other run is dropped |
agent_id-{today} |
One event per agent per day, exactly the symptom that hides real volume |
task_type |
One event per task type per 24h across the whole org |
| A constant string | One event per 24h, ever |
The official SDKs already do this for you: @humanhours/sdk and humanhours (Python) attach a fresh UUID to every track() unless you pass your own. The n8n community node uses the execution id by default. If you are calling /v1/track directly with curl or a hand-rolled HTTP client, either send a unique value yourself or omit the header entirely so the server records every request.
Rate limits
| Plan | Per second per API key |
|---|---|
| Free | 60 |
| Pro | 200 |
| Enterprise | 1 000 |
429 responses include Retry-After: 1 per RFC 6585.