punchline!

API

TypeScript SDK

@punchline-analytics/sdk is a typed client for the API. Its methods and types are generated from the same OpenAPI spec the API is tested against, so they always match. It works in Node.js 18+, Deno, Bun and edge runtimes: anywhere with fetch. It has no dependencies.

npm install @punchline-analytics/sdk

MIT licensed. Every example in the API reference is also a plain HTTP request, if you'd rather not add a dependency.

Set up

import { Punchline } from "@punchline-analytics/sdk";

const punchline = new Punchline({ apiKey: process.env.PUNCHLINE_API_KEY! });
Option Default
apiKey (required) A key from Site settings → API and AI access (keys)
baseUrl https://app.punchlineanalytics.com
maxRetries 2 Retries after a 429, waiting for Retry-After (up to 60 s each)
fetch global fetch Pass your own for proxies or tests

The constructor checks the key's format, so a pasted-wrong key fails at startup instead of on the first request. Keep the key server-side (why).

Query

const { results, meta } = await punchline.query({
  site_id: process.env.PUNCHLINE_SITE_ID!,
  date_range: { from: "2026-09-01", to: "2026-09-30" },
  metrics: ["sessions", "agent_share"],
  dimensions: ["day"],
});

for (const row of results) console.log(row.day, row.sessions, row.agent_share);
if (meta.suppressed_rows) console.log(`${meta.suppressed_rows} small groups withheld for privacy`);

The query cookbook has a body for most questions.

Reports

const accounts = await punchline.getAccounts({ from: "2026-09-24", to: "2026-09-30", min_strength: "strong" });
const funnel = await punchline.getContentFunnel({ group_by: "content_type" });
const influence = await punchline.getAiInfluence({ goal: "Purchase" });

Every agent session

allSessions() follows next_cursor for you:

for await (const session of punchline.allSessions({ from: "2026-09-24", to: "2026-09-30" })) {
  console.log(session.started_at, session.operator, session.type, session.paths);
}

Sending events

From your backend, with a key that has Can send events:

await punchline.sendEvents(
  {
    site_id: process.env.PUNCHLINE_SITE_ID!,
    events: [{ name: "purchase", value: 249, path: "/checkout", visitor: { ip: req.ip, user_agent: req.headers["user-agent"] } }],
  },
  { idempotencyKey: `order-${order.id}` },
);

visitor ties the event to the visit it came from, so it's counted as a person or an agent like the visit was. The IP address and user agent are used in memory only and never stored. See Events and goals.

const { data } = await punchline.createHandoff({
  site_id: process.env.PUNCHLINE_SITE_ID!,
  kind: "cart",
  value: cart.total,
  currency: "USD",
  url: `/cart/${cart.id}`,
  visitor: { ip: agentIp, user_agent: agentUserAgent },
});
// Return data.url to the agent.

See Handoff links.

Managing alerts and goals

With a key that has Can manage:

const { data: alert } = await punchline.createAlert(
  { name: "Agent share spike", kind: "anomaly", metric: "agent_share", op: "up", value: 50 },
  { idempotencyKey: "agent-share-spike" },
);
await punchline.updateAlert(alert!.id!, { value: 80 });

Errors

Every failed request throws a PunchlineError:

import { Punchline, PunchlineError } from "@punchline-analytics/sdk";

try {
  await punchline.query(body);
} catch (err) {
  if (err instanceof PunchlineError) {
    console.error(err.status, err.code, err.param, err.message); // 400 invalid_parameter limit "limit: must be at most 1000"
    if (err.code === "insufficient_scope") {
      /* this key can't do that */
    }
  } else throw err; // network errors
}

err.code is one of the error codes; err.docsUrl links to its explanation. A 429 is retried before it throws.

Methods

One method per endpoint, named after the endpoint's operationId:

Method Endpoint Scope
query(body) POST /v1/query read
sendEvents(body, opts?) POST /v1/events events:write
createHandoff(body) POST /v1/handoffs events:write
getHandoffs(params?) GET /v1/handoffs read
listSites() GET /v1/sites read
getSite(id) GET /v1/sites/{id} read
listOperators() GET /v1/operators read
listSessions(params?) GET /v1/sessions read
getSession(id) GET /v1/sessions/{id} read
getSessionEvidence(id) GET /v1/sessions/{id}/evidence read
getContentFunnel(params?) GET /v1/content/funnel read
getRecommendations(params?) GET /v1/content/recommendations read
getAccounts(params?) GET /v1/accounts read
getAiInfluence(params?) GET /v1/ai-influence read
getResolution(params?) GET /v1/resolution read
getReconciliation(params?) GET /v1/reconciliation read
listReports() GET /v1/reports read
createExport(body) POST /v1/exports read
getExport(id) GET /v1/exports/{id} read
downloadExport(id) GET /v1/exports/{id}/download read
listAlerts() GET /v1/alerts admin
createAlert(body, opts?) POST /v1/alerts admin
getAlert(id) GET /v1/alerts/{id} admin
updateAlert(id, body, opts?) PATCH /v1/alerts/{id} admin
deleteAlert(id, opts?) DELETE /v1/alerts/{id} admin
listGoals() GET /v1/goals admin
createGoal(body, opts?) POST /v1/goals admin
getGoal(id) GET /v1/goals/{id} admin
updateGoal(id, body, opts?) PATCH /v1/goals/{id} admin
deleteGoal(id, opts?) DELETE /v1/goals/{id} admin
listTriggers() GET /v1/triggers admin
createTrigger(body, opts?) POST /v1/triggers admin
getTrigger(id) GET /v1/triggers/{id} admin
updateTrigger(id, body, opts?) PATCH /v1/triggers/{id} admin
deleteTrigger(id, opts?) DELETE /v1/triggers/{id} admin
getUrlParameters() GET /v1/url-parameters admin
updateUrlParameters(body, opts?) PATCH /v1/url-parameters admin
deleteUrlParameters(opts?) DELETE /v1/url-parameters admin
allSessions(params?) Every page of GET /v1/sessions read

Arguments: id is the path's ID, params the query-string parameters, body the JSON body, and opts.idempotencyKey sets Idempotency-Key. Every parameter and field is typed; your editor shows what each takes. The API reference has the details and example responses.

Ask the docs

Answers come from these docs only, written by Claude (Anthropic). Your question is sent to answer it, and isn't stored.