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 i @whizz/scribeQuickstart#
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#
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#
| Field | Type | Description |
|---|---|---|
| articles.create(params, opts?) | → JobAck | POST /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? }) | → Article | GET /v1/articles/{id}. format: "markdown" | "html" narrows the payload. |
| articles.publish(id, { integrationId, asDraft? }) | → PublishResult | POST /v1/articles/{id}/publish. |
| jobs.get(id) | → Job | GET /v1/jobs/{id} with live stages. |
| jobs.waitFor(id, opts?) | → Job | Polls with growing backoff until the job succeeds — details below. |
| brandProfiles.create(params) | → BrandProfileAck | POST /v1/brand-profiles, including the auto-ingest behavior. |
| brandProfiles.list() | → List<BrandProfileSummary> | GET /v1/brand-profiles. |
| brandProfiles.addSource(id, params) | → SourceAck | POST /v1/brand-profiles/{id}/sources. |
| brandProfiles.listSources(id) | → List<KbSource> | GET /v1/brand-profiles/{id}/sources with ingestion status. |
| usage.get({ days? }) | → UsageReport | GET /v1/usage. |
| webhooks.create({ url, events? }) | → WebhookEndpointCreated | POST /v1/webhooks. The returned secret appears only here. |
| webhooks.list() | → List<WebhookEndpointSummary> | GET /v1/webhooks. |
| schedules.create(params) | → Schedule | POST /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) | → Schedule | GET /v1/schedules/{id}. |
| schedules.update(id, params) | → Schedule | PATCH /v1/schedules/{id}. Pausing is always allowed; resuming needs the capability on. |
| schedules.delete(id) | → ScheduleDeleted | DELETE /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:
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:
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:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await scribe.usage.get({ days: 30, signal: controller.signal });
// aborting also cancels retry sleeps and waitFor pollingWebhook verification#
The exact Standard Webhooks scheme — HMAC-SHA256 over id.timestamp.body with the base64-decoded secret, constant-time comparison, 5-minute replay tolerance:
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 });
}