TypeScript / JavaScript SDK
The official npm SDK: typed monitor and task methods, batches, webhooks and error classes for Node and edge runtimes.
Install
npm install @answerline/sdk
Example
import { Client } from "@answerline/sdk";
const client = new Client({ apiKey: process.env.API_KEY! });
const res = await client.monitor.chatgpt<{ text: string }>({
prompt: "What are the best running shoes for flat feet?",
country: "US",
});
console.log(res.result.text);
Every endpoint is a typed method. Monitor calls return the parsed result, task calls return ids you poll or receive by webhook, and batches queue up to 500 requests at once.
Errors are thrown as ApiError with the HTTP status, API error code and request id, and safe retries are built in.
What the SDK covers
@answerline/sdk is a dependency-free client over the REST API. Every endpoint has a method, requests and responses keep the wire field names (taskType, idempotencyKey, creditsCharged), and the retry policy from the authentication and errors guide is built in. Install it from npm:
npm install @answerline/sdk
It targets Node 18+, Deno, Bun and browsers, uses the runtime’s fetch, and one Client instance can serve any number of concurrent calls.
Configuration
The SDK reads no environment variables. Everything goes to the constructor, which works in serverless and edge runtimes where process.env may not exist.
import { Client } from "@answerline/sdk";
const client = new Client({
apiKey: process.env.API_KEY!, // required, sk_...
baseUrl: "https://api.answerline.dev", // default
timeoutMs: 30_000, // per attempt, most calls
syncTimeoutMs: 360_000, // per attempt, monitor calls and task creation
maxRetries: 2, // retries after the first attempt
});
| Option | Default | Applies to |
|---|---|---|
apiKey |
none (required) | Sent as Authorization: Bearer <key> |
baseUrl |
https://api.answerline.dev |
Every call |
timeoutMs |
30 s | Each attempt of GET calls such as getTask, credits |
syncTimeoutMs |
360 s | Each attempt of monitor calls, createTask, createBatch |
maxRetries |
2 | Retries after the first attempt |
fetch |
global fetch |
Inject a custom implementation (tests, instrumentation) |
syncTimeoutMs is long because a synchronous call can take up to five minutes, and the client should not give up just before the answer arrives. Every per-call method also accepts a RequestOptions object with signal, timeoutMs, maxRetries and headers, covered under retries and cancellation.
Synchronous monitor calls
A monitor call sends one request to POST /v1/monitor/<engine> and resolves with { success, result } once the engine has answered. Three spellings are equivalent:
// Namespaced, one method per engine
await client.monitor.chatgpt({ prompt: "Best CRM for a 5-person team?", country: "US" });
// Generic form, engine as a string
await client.monitor("gemini", { prompt: "Best CRM for a 5-person team?", country: "US" });
// Top-level shortcuts
await client.gemini({ prompt: "Best CRM for a 5-person team?", country: "US" });
The namespaced methods are chatgpt, perplexity, gemini, copilot, grok, aimode, google and googleNews. The top-level shortcuts are the same, except that AI Mode is aiMode and Google News is googleNews. The generic form takes the route: "chatgpt", "gemini", "grok", "google", "google/news", "copilot", "perplexity" or "aimode". Perplexity and Grok are paused for now and coming back soon; until then calls to them are refused and not billed.
Results are typed as unknown unless you supply a type parameter. Declare only the fields you read; the full shape per engine is on each engine page, for example ChatGPT.
interface ChatGptResult {
text: string;
sources?: { position: number; url: string; label: string }[];
searchQueries?: string[];
}
const { result } = await client.monitor.chatgpt<ChatGptResult>({
prompt: "Which accounting software do freelancers recommend?",
country: "US",
include: { markdown: true, searchQueries: true },
});
for (const source of result.sources ?? []) console.log(source.position, source.url);
Google News, Google Search and AI Mode
Google News takes a query plus country or gl, and optional hl and pages:
const news = await client.monitor.googleNews<{ newsResults: { position: number; source: string; title: string }[] }>({
query: "electric vehicle tariffs",
country: "US",
hl: "en",
pages: 2,
});
Google Search and AI Mode follow the same pattern: client.monitor.google({ query, country: "US", pages: 2, include: { aioverview: { markdown: true } } }) returns organicResults and the AI Overview, and client.monitor.aimode({ prompt, gl: "us", hl: "en" }) returns the AI Mode answer.
aimode and googleNews require either country or gl, which the types enforce. To resolve a Google redirect link, client.googleGoto(url) returns the destination URL as a string and uses a 15-second timeout.
What a call costs
A synchronous call adds 7 credits to the engine’s price, and on Google Search and Google News 18.9 more per results page; the same request as an async task does not.
| Request | Async task | Synchronous call |
|---|---|---|
| ChatGPT, base | 6.2 | 13.2 |
ChatGPT with rawResponse or shopping (+7 once) |
13.2 | 20.2 |
| Gemini | 5 | 12 |
| Copilot | 47.3 | 54.3 |
| AI Mode | 37.8 | 44.8 |
| Google Search, first page | 18.9 | 44.8 |
| Google News, first page | 2 | 27.9 |
Extra Google Search pages cost 18.9 credits each and its AI Overview 18.9; extra Google News result pages cost 2 credits each. A synchronous call pays 18.9 more for each extra page. The credits guide has the complete table.
Async tasks
Tasks run the same request in the background. createTask answers at once with the task id; the task moves from QUEUED to PROCESSING to COMPLETED or FAILED.
const { task, credits } = await client.createTask({
taskType: "CHATGPT",
payload: { prompt: "Best CRM for a 5-person team?", country: "US" },
priority: 5,
idempotencyKey: "crm-us-2026-09-17",
});
console.log(task.id, task.status, credits.creditsToCharge);
taskType is one of CHATGPT, GEMINI, GROK, GOOGLE, GOOGLE_NEWS, COPILOT, PERPLEXITY or AIMODE, and payload is the body you would send to that engine’s monitor endpoint. If you omit idempotencyKey, the SDK generates one, so a retried creation never produces a duplicate task. For scheduled work, derive the key from what the task means (prompt, market, day) so rerunning a whole job is also safe; see idempotency keys explained.
Waiting for a result
getTask(id) returns { task, credits, response? }. waitForTask polls it, backing off with jitter from 500 ms up to intervalMs, and keeps polling through rate limits, server errors and network errors:
const done = await client.waitForTask(task.id, { timeoutMs: 11 * 60_000, intervalMs: 10_000 });
if (done.task.status === "COMPLETED") {
console.log(done.credits.creditsCharged, done.response);
} else {
console.error("task failed", done.response); // response holds the error; not charged
}
client.asyncTasks.run(task, waitOptions) creates and waits in one call. The asyncTasks namespace mirrors the flat methods: create, createBatch, get, wait, run, status and clearQueue.
When the overall timeoutMs passes, waitForTask rejects with an Error whose name is "TimeoutError". The task itself keeps running; call getTask later or rely on a webhook.
Batches
createBatch submits 1 to 500 tasks (MAX_BATCH) in one request. Each item is validated and admitted on its own, and results answers each by its index. A batch outside that range throws a RangeError before anything is sent.
import { Client, MAX_BATCH, type TaskInput } from "@answerline/sdk";
const prompts = ["best running shoes for flat feet", "best trail running shoes"];
const engines = ["CHATGPT", "GEMINI"] as const;
const day = new Date().toISOString().slice(0, 10);
const tasks: TaskInput[] = prompts.flatMap((prompt, p) =>
engines.map((taskType) => ({
taskType,
payload: { prompt, country: "US" },
idempotencyKey: `shoes-${p}-${taskType}-US-${day}`,
webhook: { url: "https://example.com/hooks/answers" },
})),
);
for (let i = 0; i < tasks.length; i += MAX_BATCH) {
const batch = await client.createBatch(tasks.slice(i, i + MAX_BATCH));
for (const item of batch.results) {
if (!item.success && item.error.code !== "RESOURCE_ALREADY_EXISTS") {
console.warn(item.index, item.error.code, item.error.message);
}
}
}
Item errors are VALIDATION_ERROR, RESOURCE_ALREADY_EXISTS and INSUFFICIENT_CREDITS. RESOURCE_ALREADY_EXISTS on a rerun means the task was created the first time, so it is safe to ignore. Only an overflowing queue rejects the whole batch, as a 429 QUEUE_LIMIT_EXCEEDED.
Managing the queue
const status = await client.asyncStatus();
// { queuedTasks, processingTasks, priorityBreakdown: [{ priority, count }], concurrency }
const { cleared } = await client.clearQueue(); // deletes QUEUED tasks, which were never charged
Webhooks
Add webhook: { url } to a task and the finished task is POSTed to that URL, signed with your account’s secret. The body is the same JSON getTask returns. Verify it with verifyWebhook against the raw body before parsing:
import { verifyWebhook, SIGNATURE_HEADER } from "@answerline/sdk";
const ok = await verifyWebhook(rawBody, headers.get(SIGNATURE_HEADER), process.env.WEBHOOK_SECRET!);
verifyWebhook(body, header, secret, toleranceSeconds = 300) accepts the body as a string or Uint8Array, uses WebCrypto (so it works in edge runtimes), compares in constant time, and accepts either signature during a secret rotation. Re-serialized JSON will not verify. Deliveries can arrive more than once and out of order, so deduplicate on task.id. The webhooks guide covers retries and redelivery and has an Express receiver.
Error handling
Every non-2xx response, and any response body that is not JSON, rejects with ApiError:
import { ApiError } from "@answerline/sdk";
try {
await client.monitor.chatgpt({ prompt: "Best CRM for a 5-person team?", country: "US" });
} catch (err) {
if (!(err instanceof ApiError)) throw err; // TimeoutError, abort reason or fetch error
switch (err.code) {
case "INSUFFICIENT_CREDITS":
// 403: top up or wait for the cycle reset
break;
case "CONCURRENT_LIMIT_EXCEEDED":
// 429 after retries: every slot busy; queue a task instead
break;
default:
console.error(err.status, err.code, err.meta.requestId, err.body);
}
}
| Property | Meaning |
|---|---|
status |
HTTP status |
code |
Stable error code such as RATE_LIMIT_EXCEEDED, or undefined |
body |
Parsed JSON body, or raw text |
meta |
status, headers, requestId, and numeric rateLimitLimit, rateLimitRemaining, concurrentLimit, concurrentCurrent, concurrentRemaining, creditsRemaining, creditsCharged, latencyMs |
Other failures are not wrapped: a per-attempt timeout rejects with a DOMException named TimeoutError, an aborted call rejects with the signal’s reason, and a network failure rejects with the fetch error. Quote meta.requestId when you contact support.
Retries, timeouts and cancellation
The SDK retries only what is safe to repeat:
- A 429 is always retried, because nothing ran and nothing was charged.
Retry-Afteris honored up to 8 seconds; a longer one surfaces the error. - 5xx, timeouts and network errors are retried for GET and for task creation, which always carries an idempotency key.
- Synchronous monitor calls are charged on success and may already be running when a connection drops, so they are retried after a network error only when the request never left (connection refused, host not resolved). A 500 or 502 is final.
- Other 4xx responses are never retried.
Override per call with RequestOptions:
const controller = new AbortController();
setTimeout(() => controller.abort(new Error("user navigated away")), 60_000);
await client.monitor.gemini(
{ prompt: "What is the consensus on the new tax bill?", country: "US" },
{ signal: controller.signal, maxRetries: 0, headers: { "X-Request-Id": "report-42" } },
);
signal aborts the whole call, including backoff waits. For headers the SDK has no field for, or to read rate-limit headers on a successful call, use the low-level request method:
const { data, meta } = await client.request<{ remaining: number }>("GET", "/v1/credits");
console.log(data.remaining, meta.rateLimitRemaining, meta.requestId);
Utility calls: client.credits() returns { remaining, perCycle, cycleResetsAt }, client.countries("chatgpt") lists supported country codes, and client.states("US") lists US state codes for state targeting on Gemini.
Framework recipes
Next.js route handler that receives webhooks
In the App Router, read the body with req.text() so the bytes stay untouched:
// app/api/hooks/answers/route.ts
import { verifyWebhook, SIGNATURE_HEADER } from "@answerline/sdk";
export async function POST(req: Request) {
const body = await req.text();
if (!(await verifyWebhook(body, req.headers.get(SIGNATURE_HEADER), process.env.WEBHOOK_SECRET!))) {
return new Response("invalid signature", { status: 400 });
}
const delivery = JSON.parse(body);
if (delivery.test) return new Response(null, { status: 204 }); // dashboard test delivery
// Upsert by delivery.task.id, then answer quickly.
return new Response(null, { status: 204 });
}
Do not run synchronous monitor calls inside serverless request handlers with short execution limits: a call can take up to five minutes. Create a task from the handler and let the webhook route store the result.
Nightly cron job
A script run by cron, a CI schedule or a container job:
// track.ts: compile, then run on a schedule (an ES module, for top-level await)
import { Client, type TaskInput } from "@answerline/sdk";
const client = new Client({ apiKey: process.env.API_KEY! });
const day = new Date().toISOString().slice(0, 10);
const engines = ["CHATGPT", "GEMINI"] as const;
const prompts = ["best CRM for small agencies", "HubSpot vs Pipedrive"];
const { remaining } = await client.credits();
console.log(`credits before run: ${remaining}`);
const tasks: TaskInput[] = engines.flatMap((taskType) =>
prompts.map((prompt, i) => ({
taskType,
payload: { prompt, country: "US" },
idempotencyKey: `${taskType}-${i}-US-${day}`,
webhook: { url: process.env.WEBHOOK_URL! },
})),
);
const batch = await client.createBatch(tasks);
console.log(batch.summary); // { total, succeeded, failed }
Rerunning the script on the same day creates nothing new, because every key already exists. For planning prompt sets and cadence, see AI monitoring cadence and rank tracking pipelines with batches.
The quickstart shows a first call in curl, TypeScript and Python side by side.
Questions
Is the TypeScript SDK published on npm?
Yes. @answerline/sdk is on npm and installs with npm install @answerline/sdk. It runs on Node 18 or later, Deno, Bun and browsers.
Which environment variable does the SDK read for the API key?
None. The SDK reads no environment variables; you pass apiKey to the Client constructor yourself, for example from process.env.API_KEY.
Does the SDK retry failed requests?
Yes. By default it retries up to 2 times with capped exponential backoff and full jitter. A 429 is always retried; 5xx, timeouts and network errors are retried only for GET requests and task creation, which always carries an idempotency key.
Should I use client.monitor or async tasks?
Use client.monitor for interactive work where a user waits for the answer. Use asyncTasks.create or createBatch for scheduled or large runs: tasks cost the engine's price without the synchronous surcharge, wait for a free concurrency slot instead of failing, and can be delivered by webhook.
How do I verify a webhook delivery in TypeScript?
Call verifyWebhook(rawBody, webhookSignatureHeader, secret) with the body exactly as received. It returns true only when a v1 signature matches and the timestamp is within 300 seconds of now by default.
Can I call the API from browser code with the SDK?
The SDK runs in browsers, but an API key spends your account's credits, so keep calls on a server or edge function and send the result to the browser.
Other integrations
Python SDK
The official Python SDK: monitor calls, async tasks and batches with the same response shapes as the REST API.
Hosted MCP Server
Give Claude, Cursor or any MCP client search and AI-answer tools with one configuration line.
n8n Community Node
Use AnswerLine in n8n workflows: run monitors, queue tasks and read credits as native nodes.
Zapier App
Trigger Zaps from AI answers: Run Monitor for quick calls, Create Task with a webhook callback for long-running engines.
LangChain Tools
Ready-made LangChain tools for JavaScript and Python: one monitor tool per engine, plus task and credit tools.