TypeScript SDK
Install @clment/sdk, authenticate, and work through uploads, reviews and redlines with a fully-typed client whose types come from the OpenAPI spec.
Updated 26 Aug 2026
@clment/sdk is the official TypeScript client for the Clment REST API. If you build in TypeScript or JavaScript, start here rather than hand-writing HTTP calls or generating a client — every parameter, body and response type is derived from the same OpenAPI document the API is built against, so the client cannot quietly drift from the service.
It has no dependencies. It uses the platform fetch, ships ESM and CommonJS builds with type declarations, and runs on Node 20+, Bun, Deno, edge runtimes and the browser (though your key belongs on a server — see Authentication).
npm install @clment/sdk
Your first call
import { ClmentClient } from '@clment/sdk';
const clment = new ClmentClient({
apiKey: process.env.CLMENT_API_KEY!,
region: 'nz', // ← your organisation's region
});
const { contracts, total } = await clment.contracts.list({ status: 'active', take: 20 });
console.log(`${contracts.length} of ${total} active contracts`);
Get the region right
This is the one thing worth reading twice. Your contracts and your API keys live in exactly one region, and a valid key sent to the wrong region fails exactly like an invalid one — a 401 that sends people hunting for a new key when the host was the problem.
The SDK makes the region an explicit, typed argument rather than a URL you assemble, and names it in the error when authentication fails, so the most likely integration mistake explains itself.
| Region | Code |
|---|---|
| United States | us |
| European Union | eu |
| United Kingdom | uk |
| Australia | au |
| New Zealand | nz |
| Canada | ca |
Your region is shown in Settings → API & Integrations. For a local or self-hosted API, pass baseUrl instead — it overrides region:
const clment = new ClmentClient({ apiKey, baseUrl: 'http://localhost:3000/v1' });
Uploading a contract
Uploads are asynchronous: text extraction, classification and indexing run in the background. The call returns a job id, and waitForUpload polls for you.
import { readFile } from 'node:fs/promises';
const result = await clment.contracts.upload(
{ content: await readFile('./acme-msa.pdf'), fileName: 'acme-msa.pdf' },
{ title: 'Acme — Master Services Agreement' },
);
// Two outcomes, and the types make you handle both: a new contract comes
// back as an accepted job, while a file that matched an existing contract
// is filed as a version of it there and then.
if ('jobId' in result) {
const job = await clment.waitForUpload(result.jobId!, {
onPoll: (status) => console.log(`upload: ${status}`),
});
const contract = await clment.contracts.get(job.contractId!);
console.log(contract.title, contract.status, contract.totalValue);
}
Uploads are not idempotent. Sending the same file twice creates two contracts. If your job may re-run, keep a record of what you have already sent — file name plus size is usually enough — and check the contract still exists before trusting it, since someone may have deleted it in the app.
Running a review
const { rulebooks } = await clment.rulebooks.list();
const rulebook = rulebooks!.find((r) => r.name === 'Standard SaaS — Buy-side');
const { jobId, estimateSeconds, steps } = await clment.reviews.start(contractId, {
rulebookId: rulebook!.id,
mode: 'standard',
reviewStrategy: 'negotiation',
});
console.log(`≈ ${Math.round(estimateSeconds! / 60)} min, ${steps!.length} stage(s)`);
const job = await clment.waitForReview(jobId, { intervalMs: 5_000 });
const { review } = await clment.reviews.get(job.reviewId!);
for (const finding of review!.findings ?? []) {
console.log(`[${finding.severity}] ${finding.title} — ${finding.status}`);
}
estimateSeconds comes from your region’s own measurements of comparable reviews. It is advisory — a job that outruns it is still running.
To compare two versions, run a review with scope: 'two_version'; the result carries comparisonSources naming both documents. There is no separate compare endpoint.
Recording verdicts and generating a redline
await clment.reviews.updateFinding(review!.id, findingId, { humanDecision: 'agree' });
// A redline instruction is not a note: the latest one on a finding
// replaces the AI's proposed wording in the generated document.
await clment.reviews.addFindingComment(review!.id, findingId, {
text: 'Cap it at 12 months of fees, matching clause 11.1',
kind: 'redline_instruction',
});
const { jobId } = await clment.reviews.generateRedline(contractId, {
playbookName: rulebook!.name,
sourceReviewId: review!.id,
findings: (review!.findings ?? []).filter((f) => f.includeInRedline),
commentLevel: 'brief',
});
const redline = await clment.waitForRedline(jobId);
const docx = await clment.reviews.downloadRedline(jobId); // the job id is the download token
Errors
Every failure throws a typed error carrying the HTTP status, the API’s stable code, and the parsed body — so you branch on a value, not on a message.
import { ClmentAuthError, ClmentRateLimitError, isClmentError } from '@clment/sdk';
try {
await clment.contracts.list();
} catch (err) {
if (err instanceof ClmentAuthError) {
console.error(err.message, err.region); // explains the region trap
} else if (err instanceof ClmentRateLimitError) {
await sleep((err.retryAfterSeconds ?? 60) * 1_000);
} else if (isClmentError(err)) {
console.error(err.status, err.code, err.body);
} else {
throw err; // network failure or abort — nothing to branch on
}
}
| Class | When |
|---|---|
ClmentAuthError | 401 / 403 — usually the wrong region |
ClmentRateLimitError | 429, with retryAfterSeconds |
ClmentError | every other non-2xx (status, code, body) |
ClmentConnectionError | the request never arrived (DNS, TLS, abort) |
ClmentConfigError | bad construction, or a polling timeout |
Retry the transient ones — 429, 5xx and connection failures. A 401, 403 or 404 will not fix itself.
Rate limits and credits
Rate-limit headers ride on every error, and onRateLimit fires for successful responses too, so you can slow down before you are throttled rather than after:
const clment = new ClmentClient({
apiKey,
region: 'nz',
onRateLimit: ({ limit, remaining }) => {
if (remaining !== null && limit !== null && remaining < limit * 0.1) throttle();
},
});
AI operations consume credits; reads are free. Check where you stand before a batch, rather than discovering exhaustion as a 402 halfway through one:
const usage = await clment.usage.current();
Pagination
List endpoints take skip and take (capped at 100) and return a total, so paging is a loop:
const all = [];
for (let skip = 0; ; skip += 100) {
const page = await clment.contracts.list({ take: 100, skip });
all.push(...(page.contracts ?? []));
if ((page.contracts?.length ?? 0) < 100) break;
}
What else is there
| Resource | What it covers |
|---|---|
contracts | upload, read, update, delete, versions, text, activity, tags, bulk operations, custom dates |
reviews | start, read, verdicts, comments and redline instructions, assignment, redlines |
rulebooks | list, read, create, delete |
jobs | review and redline job status |
search | ranked search across contracts |
ask | natural-language questions across the portfolio |
keyDates | the portfolio date stream, and reminders |
collaborators | the people you can tag or assign to |
me | your mentions, and the organisation at a glance |
usage, insights, taxonomy, convert, chatSessions | credits, KPIs, the classification tree, PDF→Word, saved conversations |
Anything the SDK doesn’t wrap
Including an endpoint newer than your installed version — is reachable raw, with the envelope and headers intact:
const res = await clment.request({
method: 'GET',
path: '/contracts/{id}',
pathParams: { id: contractId },
});
console.log(res.status, res.data, res.rateLimit);
Other client options: timeoutMs (default 30s; uploads and conversions get longer defaults), headers, userAgent (appended to the SDK’s own — please name your integration), fetch (inject your own), and signal per request.
Building in another language
There are two more official clients, generated from the same OpenAPI document:
Both are thinner than this one — no waitFor helpers, and the region is a base URL rather than a typed argument — but exhaustive, and they cannot drift from the API.
For Go, Java and the rest, generate a client from the same document — see Developers for the one-liner.