Node.js and TypeScript
A small typed client around fetch with retries, idempotent submits and a wait helper, ready to drop into a service or a serverless function.
No SDK is needed; the API is plain JSON over HTTPS and fetch is built into Node 18 and later. This page is a complete client you can copy.
The client
export type CheckOptions = Partial<{
plagiarism: boolean;
grammarSpelling: boolean;
citations: boolean;
aiContent: boolean;
style: boolean;
register: 'academic' | 'general';
}>;
export interface CheckSummary {
id: string;
status:
| 'queued'
| 'extracting'
| 'analyzing'
| 'searching'
| 'scoring'
| 'done'
| 'rejected'
| 'cancelled'
| 'failed';
progress: number;
phase: string;
title: string | null;
wordCount: number | null;
originalityScore: number | null;
coverage: number | null;
scores: {
plagiarism: number;
grammarSpelling: number | null;
citation: number | null;
aiContent: number | null;
clarity: number | null;
} | null;
issueCounts: { grammar: number; citations: number } | null;
rejection: { code: string; message: string } | null;
}
const FINAL = new Set(['done', 'rejected', 'failed', 'cancelled']);
export class Silvertext {
constructor(
private readonly key = process.env.SILVERTEXT_API_KEY!,
private readonly base = 'https://api.silvertext.com',
) {}
async request<T>(path: string, init: RequestInit = {}, attempts = 4): Promise<T> {
let delay = 1000;
for (let attempt = 1; ; attempt++) {
let res: Response | undefined;
try {
res = await fetch(this.base + path, {
...init,
headers: {
Authorization: `Bearer ${this.key}`,
'Content-Type': 'application/json',
...(init.headers ?? {}),
},
});
} catch (err) {
if (attempt >= attempts) throw err;
}
if (res?.ok) return (res.status === 204 ? null : await res.json()) as T;
const retryable = !res || res.status === 429 || res.status >= 500;
if (!retryable || attempt >= attempts) {
const body = await res?.json().catch(() => null);
throw new SilvertextError(
res?.status ?? 0,
body?.error?.code ?? 'network',
body?.error?.message ?? 'request failed',
);
}
const retryAfter = Number(res?.headers.get('Retry-After'));
await new Promise((r) =>
setTimeout(r, retryAfter > 0 ? retryAfter * 1000 : delay + Math.random() * 500),
);
delay = Math.min(delay * 2, 30_000);
}
}
/** Submit text. `idempotencyKey` makes a retry safe. */
createCheck(
body: {
text?: string;
file?: string;
filename?: string;
title?: string;
options?: CheckOptions;
citationStyle?: 'APA' | 'MLA' | 'Chicago';
},
idempotencyKey?: string,
) {
return this.request<{ id: string }>('/v1/checks', {
method: 'POST',
body: JSON.stringify(body),
headers: idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {},
});
}
getCheck<T = CheckSummary>(
id: string,
query: { view?: 'summary' | 'full'; include?: string[] } = {},
) {
const q = new URLSearchParams();
if (query.include) q.set('include', query.include.join(','));
else if (query.view) q.set('view', query.view);
return this.request<T>(`/v1/checks/${id}${q.size ? `?${q}` : ''}`);
}
cancelCheck(id: string) {
return this.request<null>(`/v1/checks/${id}/cancel`, { method: 'POST' });
}
deleteCheck(id: string) {
return this.request<null>(`/v1/checks/${id}`, { method: 'DELETE' });
}
usage() {
return this.request<{ balanceAvailableCredits: number; balanceHeldCredits: number }>(
'/v1/me/usage',
);
}
/** Poll until the check is final, cancelling it at the deadline so no credits stay reserved. */
async waitForCheck(
id: string,
{ deadlineMs = 10 * 60_000, signal }: { deadlineMs?: number; signal?: AbortSignal } = {},
) {
const started = Date.now();
let delay = 3000;
for (;;) {
const check = await this.getCheck(id, { view: 'summary' });
if (FINAL.has(check.status)) return check;
if (signal?.aborted || Date.now() - started > deadlineMs) {
await this.cancelCheck(id).catch(() => {});
throw new Error(`check ${id} did not finish in time`);
}
await new Promise((r) => setTimeout(r, delay));
if (Date.now() - started > 60_000) delay = Math.min(delay + 2000, 10_000);
}
}
}
export class SilvertextError extends Error {
constructor(
public status: number,
public code: string,
message: string,
) {
super(message);
}
}Using it
import { readFile } from 'node:fs/promises';
import { Silvertext } from './silvertext';
const st = new Silvertext();
// Text
const { id } = await st.createCheck(
{ title: 'Q3 launch post', text: draft, options: { citations: false } },
`post:${postId}:v${version}`,
);
const summary = await st.waitForCheck(id);
if (summary.status !== 'done') throw new Error(summary.rejection?.message ?? summary.status);
// The findings
const report = await st.getCheck<{
report: { issues: Array<{ category: string; fix?: { replacement: string } }> };
}>(id, {
include: ['issues', 'sources'],
});
const grammar = report.report.issues.filter((i) => i.category === 'grammar');
// A file
const bytes = await readFile('thesis-draft.docx');
const file = await st.createCheck({
file: bytes.toString('base64'),
filename: 'thesis-draft.docx',
});Serverless and edge functions
A check outlives a single short-lived function. Submit from one invocation, store the id, and read the result from a later one (a queue message, a scheduled function, or the user's next request), rather than holding a function open to poll. The Idempotency-Key makes the submit step safe to run twice when a queue redelivers.