DOCS / API / TRACK
VIEW RAW

POST /v1/track

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.


Found a typo or want to suggest an edit? Email support@triadagency.ai.