DOCS / SDKS / JAVASCRIPT
VIEW RAW

JavaScript / TypeScript SDK

Preview. The npm package is not published yet. Examples on this page work today against https://humanhours.dev/api/v1/*. For now, copy the snippet you need or use plain fetch against the documented endpoints; the published @humanhours/sdk package is on the way.

# Once the package ships:
npm install @humanhours/sdk

60-second quickstart

import { Humanhours } from "@humanhours/sdk";
 
const hh = new Humanhours({ apiKey: process.env.HUMANHOURS_API_KEY! });
 
await 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:

await 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.

Time + track in one call

const result = await hh.withTask(
  { agent_id: "support-classifier", task_type: "email_classification" },
  () => classifyEmail(subject),
);

withTask measures wall-clock time for the wrapped fn, captures the outcome, and tracks it. Throws inside fn are reported as outcome="failure" and re-raised so your control flow is unchanged.

Reading numbers back

await hh.summary({ period: "30d" });
await hh.agents();
await hh.reports.raw("/time-saved?period=30d&group_by=agent");

Idempotency

Every track() call sends a fresh UUID Idempotency-Key so each run is its own event. Pass your own value only when you have a per-run identifier you want to deduplicate retries against, like an inbound message id or workflow execution id:

await hh.track({...}, { idempotencyKey: messageId });

The server returns the original event_id on a replay within 24h. Do not pass agent_id or any value that repeats across runs — that would dedupe every subsequent run to the first event of the day.

Errors

HumanhoursError carries the structured error code from the API.

import { HumanhoursError } from "@humanhours/sdk";
 
try {
  await hh.track({...});
} catch (e) {
  if (e instanceof HumanhoursError && e.code === "unknown_task_type") {
    // pass human_baseline_minutes inline, or POST /v1/task-types to register one
  }
  throw e;
}

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)
const { company, charged } = await hh.companies.enrich({
  domain: "stripe.com",
  external_id: "acct_123",
  tags: ["outreach-q3"],
});
 
// Read the outreach-ready business case
const bc = company.business_case;
console.log(bc.summary); // narrative to drop into a first-touch message
console.log(bc.annual_labour_cost_eur); // estimated annual wage bill
console.log(company.confidence.overall); // 0..1, how much to lean on the numbers
 
// Re-enrich when data may be stale (always charges a lookup)
await hh.companies.enrich({ domain: "stripe.com" }, { refresh: true });
// or equivalently:
await hh.companies.refresh("stripe.com");
 
// Bulk-enqueue a list, asynchronously (nothing charged until the job runs)
const job = await hh.companies.bulk({
  domains: ["stripe.com", "vercel.com", "supabase.com"],
  tags: ["outreach-q3"],
});
console.log(job.accepted, job.status); // e.g. 3 "queued"
 
// Poll the job until it finishes
let status = await hh.jobs.get(job.job_id);
while (status.status !== "done") {
  console.log(`${status.percent}%`);
  await new Promise((r) => setTimeout(r, 3000));
  status = await hh.jobs.get(job.job_id);
}
 
// List your library (free, no credit)
const { companies } = await hh.companies.list({ tag: "outreach-q3", limit: 500 });
 
// Fetch one record (free)
const record = await hh.companies.get("stripe.com");
 
// Export full library as JSON (free)
const { companies: all } =
  (await hh.companies.export()) as import("@humanhours/sdk").ListCompaniesResponse;
 
// Export as CSV string (free)
const csv = (await hh.companies.export({ format: "csv", tag: "outreach-q3" })) as string;

Errors surface as HumanhoursError. The most common one is lookup_quota_exceeded (HTTP 402), which means your monthly cap is reached and you need to upgrade:

import { HumanhoursError } from "@humanhours/sdk";
 
try {
  await hh.companies.enrich({ domain: "example.com" });
} catch (e) {
  if (e instanceof HumanhoursError && e.code === "lookup_quota_exceeded") {
    // Monthly cap hit — upgrade at /billing to continue enriching
  }
  throw e;
}

See the Companies API reference for the full record shape, CSV columns, and bulk jobs.

Runtime support

Native fetch, zero dependencies. Works on Node 18+, Deno, Bun, Cloudflare Workers, Vercel Edge, modern browsers.


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