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.

RegionCode
United Statesus
European Unioneu
United Kingdomuk
Australiaau
New Zealandnz
Canadaca

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
  }
}
ClassWhen
ClmentAuthError401 / 403 — usually the wrong region
ClmentRateLimitError429, with retryAfterSeconds
ClmentErrorevery other non-2xx (status, code, body)
ClmentConnectionErrorthe request never arrived (DNS, TLS, abort)
ClmentConfigErrorbad 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

ResourceWhat it covers
contractsupload, read, update, delete, versions, text, activity, tags, bulk operations, custom dates
reviewsstart, read, verdicts, comments and redline instructions, assignment, redlines
rulebookslist, read, create, delete
jobsreview and redline job status
searchranked search across contracts
asknatural-language questions across the portfolio
keyDatesthe portfolio date stream, and reminders
collaboratorsthe people you can tag or assign to
meyour mentions, and the organisation at a glance
usage, insights, taxonomy, convert, chatSessionscredits, 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:

  • .NETdotnet add package Clment.Sdk
  • Pythonpip install clment-sdk

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.

Still have questions?

Instant article search