Docs · Reference · SDK

The TypeScript SDK.

@whizz/scribe wraps the whole v1 surface in one dependency-free, fetch-based module. Node 18+, Bun, Deno, and edge runtimes; typed exactly against the wire format.

Install#

npm
npm i @whizz/scribe

Quickstart#

generate.ts
import { WhizzScribe, ScribeError } from "@whizz/scribe";

const scribe = new WhizzScribe({ apiKey: process.env.SCRIBE_KEY });

const profile = await scribe.brandProfiles.create({
  name: "Acme Analytics",
  website_url: "https://www.acme-analytics.com",
});

const ack = await scribe.articles.create(
  {
    topic: "How B2B SaaS teams actually cut churn with product analytics",
    target_keyword: "reduce saas churn",
    brand_profile_id: profile.id,
  },
  { idempotencyKey: "churn-article-001" },
);

const job = await scribe.jobs.waitFor(ack.id, {
  onProgress: (j) => console.log("stage:", j.stage),
});

const article = await scribe.articles.get(job.result.article_id, {
  format: "markdown",
});
console.log(article.title, article.word_count, article.quality_score);

Constructor#

Options
const scribe = new WhizzScribe({
  apiKey: "wz_live_…",                       // required
  baseUrl: "https://scribe.whizztech.ai", // default
  maxRetries: 2,                             // 429/5xx retries per request
  fetch: customFetch,                        // optional instrumentation hook
});

Built-in resilience: any 429 or 5xx is retried up to maxRetries times (default 2), honoring the retry-after header when present and exponential backoff when not. Network-level failures retry the same way. 4xx errors other than 429 never retry.

API surface#

FieldTypeDescription
articles.create(params, opts?)→ JobAckPOST /v1/articles. params mirrors the request body exactly (snake_case). opts.idempotencyKey sets the Idempotency-Key header.
articles.list({ limit? })→ List<ArticleSummary>GET /v1/articles.
articles.get(id, { format? })→ ArticleGET /v1/articles/{id}. format: "markdown" | "html" narrows the payload.
articles.publish(id, { integrationId, asDraft? })→ PublishResultPOST /v1/articles/{id}/publish.
jobs.get(id)→ JobGET /v1/jobs/{id} with live stages.
jobs.waitFor(id, opts?)→ JobPolls with growing backoff until the job succeeds — details below.
brandProfiles.create(params)→ BrandProfileAckPOST /v1/brand-profiles, including the auto-ingest behavior.
brandProfiles.list()→ List<BrandProfileSummary>GET /v1/brand-profiles.
brandProfiles.addSource(id, params)→ SourceAckPOST /v1/brand-profiles/{id}/sources.
brandProfiles.listSources(id)→ List<KbSource>GET /v1/brand-profiles/{id}/sources with ingestion status.
usage.get({ days? })→ UsageReportGET /v1/usage.
webhooks.create({ url, events? })→ WebhookEndpointCreatedPOST /v1/webhooks. The returned secret appears only here.
webhooks.list()→ List<WebhookEndpointSummary>GET /v1/webhooks.
schedules.create(params)→ SchedulePOST /v1/schedules. See Schedules. Throws autonomous_mode_disabled (403) while the capability is off.
schedules.list({ limit? })→ List<Schedule>GET /v1/schedules.
schedules.get(id)→ ScheduleGET /v1/schedules/{id}.
schedules.update(id, params)→ SchedulePATCH /v1/schedules/{id}. Pausing is always allowed; resuming needs the capability on.
schedules.delete(id)→ ScheduleDeletedDELETE /v1/schedules/{id}. Produced articles are untouched.
verifyWebhookSignature(payload, headers, secret, opts?)→ Promise<boolean>Standalone export — verifies deliveries with Web Crypto; no client instance needed.

jobs.waitFor#

The helper that removes all polling boilerplate — backoff, timeout, cancellation, progress:

waitFor
const job = await scribe.jobs.waitFor(ack.id, {
  pollMs: 1500,        // first interval; grows ~1.6x per poll, capped at 10s
  timeoutMs: 900_000,  // 15 min default; the runner's hard ceiling is 30 min
  onProgress: (j) => render(j.stages),
  signal: controller.signal,
});
// resolves with the succeeded job
// throws ScribeError: code "job_failed" | "job_canceled" | "poll_timeout"

Error handling#

Every non-2xx response (after retries) throws a typed ScribeError carrying the error envelope:

ScribeError
try {
  await scribe.articles.create({ topic: "x" }); // too short
} catch (err) {
  if (err instanceof ScribeError) {
    err.status;     // 400  (0 for client-side waitFor failures)
    err.code;       // "invalid_request"
    err.message;    // "topic: String must contain at least 4 character(s)"
    err.retryAfter; // seconds, present on 429s
  }
}

Cancellation#

Every method accepts an AbortSignal. Aborting cancels in-flight requests, retry sleeps, and waitFor polling loops:

AbortSignal
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);

await scribe.usage.get({ days: 30, signal: controller.signal });
// aborting also cancels retry sleeps and waitFor polling

Webhook verification#

The exact Standard Webhooks scheme — HMAC-SHA256 over id.timestamp.body with the base64-decoded secret, constant-time comparison, 5-minute replay tolerance:

app/hooks/scribe/route.ts
import { verifyWebhookSignature } from "@whizz/scribe";

// e.g. in a Next.js route handler
export async function POST(req) {
  const payload = await req.text(); // RAW body — do not JSON.parse first
  const ok = await verifyWebhookSignature(
    payload,
    Object.fromEntries(req.headers),
    process.env.SCRIBE_WEBHOOK_SECRET, // whsec_…
  );
  if (!ok) return new Response(null, { status: 401 });

  const { event, data } = JSON.parse(payload);
  // ack fast; process on a queue
  return Response.json({ received: true });
}