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.
Handoff links
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.