DOCS / SDKS / PYTHON
VIEW RAW

Python SDK

Live on PyPI as humanhours. All examples on this page work today against https://humanhours.dev/api/v1/*.

pip install humanhours

60-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,
)

See Model cost and net ROI.

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

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