Live on PyPI as
humanhours. All examples on this page work today againsthttps://humanhours.dev/api/v1/*.
pip install humanhours60-second quickstart
from humanhours import Humanhours
hh = Humanhours(api_key="hh_live_...")
hh.track(
agent_id="support-classifier",
task_type="email_classification",
outcome="success",
)Pass model plus token counts and the run cost is priced for you and subtracted into net_saved (no agent_cost needed). Use the OpenRouter model id:
hh.track(
agent_id="support-classifier",
task_type="email_classification",
outcome="success",
model="anthropic/claude-opus-4.8",
tokens_in=1800,
tokens_out=120,
)Decorator
from humanhours import Humanhours, track
hh = Humanhours(api_key="...", default_agent_id="support-classifier")
@track(hh, task_type="email_classification")
def classify(subject: str) -> str:
...The decorator times the function, captures outcome="success" on return or outcome="failure" on raise, and reports automatically. Failures re-raise — your code keeps its normal control flow.
Ambient client
with Humanhours(api_key="...") as hh:
@track(task_type="contract_clause_review", agent_id="legal-clause-reviewer")
def review_clause(text):
...
review_clause("...")@track without client= picks up the active with scope. Useful when you can't pass the client through a deep call tree.
Reading numbers back
hh.summary(period="30d")
hh.agents()
hh.report("/time-saved", {"period": "30d", "group_by": "agent"})Companies
Enrich domains, manage your library, and pull data for outreach. Lookup credits are consumed only on new enrichment and explicit refreshes; listing and exporting are free.
# Enrich one domain, synchronously (charges one lookup on first call)
result = hh.companies_enrich(
"stripe.com",
external_id="acct_123",
tags=["outreach-q3"],
)
company = result["company"]
charged = result["charged"]
# Read the outreach-ready business case
bc = company["business_case"]
print(bc["summary"]) # narrative to drop into a first-touch message
print(bc["annual_labour_cost_eur"]) # estimated annual wage bill
print(company["confidence"]["overall"]) # 0..1, how much to lean on the numbers
# Re-enrich when data may be stale (always charges a lookup)
hh.companies_enrich("stripe.com", refresh=True)
# or equivalently:
hh.companies_refresh("stripe.com")
# Bulk-enqueue a list, asynchronously (nothing charged until the job runs)
job = hh.companies_bulk(
["stripe.com", "vercel.com", "supabase.com"],
tags=["outreach-q3"],
)
print(job["accepted"], job["status"]) # e.g. 3 "queued"
# Poll the job until it finishes
import time
while True:
status = hh.jobs_get(job["job_id"])
if status["status"] == "done":
break
print(status["percent"], "%")
time.sleep(3)
# List your library (free, no credit)
data = hh.companies_list(tag="outreach-q3", limit=500)
print(data["companies"])
# Fetch one record (free)
record = hh.companies_get("stripe.com")
# Export full library as JSON (free)
data = hh.companies_export()
# Export as CSV string (free)
csv_text = hh.companies_export(format="csv", tag="outreach-q3")When the monthly research-lookup cap is reached, HumanhoursError is raised with code="lookup_quota_exceeded" and HTTP status 402. There are no overages: upgrade at /billing to raise the cap.
See the Companies API reference for the full record shape, CSV columns, and bulk jobs.
Errors
from humanhours import HumanhoursError
try:
hh.track(task_type="...", agent_id="...", outcome="success")
except HumanhoursError as e:
if e.code == "unknown_task_type":
...
raise