.NET SDK
Install Clment.Sdk, authenticate, and work through uploads, reviews and redlines with a typed client generated from the OpenAPI spec.
Updated 26 Aug 2026
Clment.Sdk is the official .NET client for the Clment REST API. It is generated from the same OpenAPI document the API is built against, so it cannot quietly drift from the service — when an endpoint changes, the client changes with it.
It targets .NET 8.0 and uses HttpClient under the hood, with nullable reference types enabled. Every request and response is a concrete type, so a shape that doesn’t match the documented contract fails at deserialisation rather than surfacing as a null three layers up.
dotnet add package Clment.Sdk
Your first call
using Clment.Sdk.Api;
using Clment.Sdk.Client;
var config = new Configuration
{
// Your organisation's region: us | eu | uk | au | nz | ca
BasePath = "https://api-nz.clment.com/v1",
};
config.AccessToken = Environment.GetEnvironmentVariable("CLMENT_API_KEY");
var contracts = new ContractsApi(config);
var page = await contracts.ListContractsAsync(status: "active", take: 20);
Console.WriteLine($"{page.Data.Contracts.Count} of {page.Data.Total} active contracts");
Use AccessToken, not AddApiKey. The API authenticates with a bearer token, and AddApiKey is ignored on this client — it fails as a 401, which reads like a bad key rather than a misconfigured one.
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.
| Region | Base URL |
|---|---|
| United States | https://api-us.clment.com/v1 |
| European Union | https://api-eu.clment.com/v1 |
| United Kingdom | https://api-uk.clment.com/v1 |
| Australia | https://api-au.clment.com/v1 |
| New Zealand | https://api-nz.clment.com/v1 |
| Canada | https://api-ca.clment.com/v1 |
Your region is shown in Settings → API & Integrations. Note the api- prefix — a bare nz.clment.com does not resolve.
Uploading a contract
Uploads are asynchronous — text extraction, classification and indexing run in the background. The call returns a job id; poll until it completes.
using var stream = File.OpenRead("acme-msa.pdf");
var result = (await contracts.UploadContractAsync(
new FileParameter("acme-msa.pdf", stream),
title: "Acme MSA")).Data;
string? contractId = result.ContractId;
if (result.JobId is not null)
{
while (true)
{
var job = (await contracts.GetUploadJobAsync(result.JobId)).Data;
if (job.Status is "complete" or "completed" or "succeeded")
{
contractId = job.ContractId;
break;
}
if (job.Status == "failed")
throw new InvalidOperationException($"upload failed: {job.Error}");
await Task.Delay(2_000);
}
}
Both success shapes share one response model, so JobId is always reachable — check it rather than assuming which path you got. Small files can complete inline and return the contract directly.
Uploads are not idempotent. The same file twice creates two contracts. Keep a record of what you have sent.
Running a review
A review needs a rulebook. List them, pick one, start the review, then poll the job — reviews run for minutes, so poll on a 10-second interval rather than a tight loop.
var rulebooks = new RulebooksApi(config);
var reviews = new ReviewsApi(config);
var jobs = new JobsApi(config);
var books = (await rulebooks.ListRulebooksAsync()).Data.Rulebooks;
var started = (await reviews.StartReviewAsync(contractId, new StartReviewRequest(
rulebookId: books[0].Id,
mode: ReviewMode.Standard))).Data;
// Advisory only, but worth logging so a long wait looks intended.
Console.WriteLine($"~{Math.Round(started.EstimateSeconds / 60.0)} min");
GetReviewJob200ResponseData job;
while (true)
{
job = (await jobs.GetReviewJobAsync(started.JobId)).Data;
if (job.Status.ToString() is "Complete" or "Completed" or "Succeeded") break;
await Task.Delay(10_000);
}
var review = (await reviews.GetReviewAsync(job.ReviewId)).Data.Review;
Console.WriteLine($"{review.Findings.Count} findings");
Comparing two versions is a review with scope: two_version, not a separate endpoint.
Generating a redline
Findings carry a verdict you record, then a redline turns the accepted ones into a tracked-changes Word document.
var started = (await reviews.GenerateRedlineAsync(contractId, new GenerateRedlineRequest(
playbookName: books[0].Name,
sourceReviewId: review.Id,
findings: review.Findings.Take(5).ToList(),
commentLevel: GenerateRedlineRequest.CommentLevelEnum.Brief))).Data;
A failed redline can report complete and then 404 on download — check job.Outcome for the explanation rather than treating the 404 as a bug.
Errors
Every non-2xx throws ApiException, carrying the status and the raw body.
try
{
await contracts.ListContractsAsync();
}
catch (ApiException err) when (err.ErrorCode == 401)
{
// Almost always the wrong region, not a bad key.
throw;
}
catch (ApiException err) when (err.ErrorCode == 429)
{
var retryAfter = err.Headers.TryGetValue("Retry-After", out var v) ? v.First() : "60";
await Task.Delay(int.Parse(retryAfter) * 1_000);
}
catch (ApiException err)
{
Console.Error.WriteLine($"{err.ErrorCode}: {err.ErrorContent}");
}
ErrorCode is the HTTP status as an int; ErrorContent is the response body, which carries the API’s stable machine-readable code.
Unlike the Python client, this client does not retry for you — a 429 reaches your code immediately, which is usually what a service wants. Retry 429, 5xx and connection failures yourself. A 401, 403 or 404 will not fix itself.
Rate limits and credits
Two independent limits apply.
Credits. AI operations — reviews, redlines, conversions — consume credits from your plan’s monthly allowance, then any credit packs. Reads are free. An exhausted balance returns 402 INSUFFICIENT_CREDITS. Check UsageApi.GetUsageAsync() before a batch rather than discovering exhaustion halfway through one.
Request rate. Requests are limited per organisation on a rolling hourly window sized to your plan. Every authenticated response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 401 carries neither, because the limiter runs only once the key resolves to an organisation.
Pagination
List endpoints page with skip and take (max 100), and return a Total:
int skip = 0, pageSize = 100;
while (true)
{
var page = (await contracts.ListContractsAsync(skip: skip, take: pageSize)).Data;
foreach (var contract in page.Contracts) { /* … */ }
skip += pageSize;
if (skip >= page.Total) break;
}
Names the generator had to escape
Where a field name would collide with something in C#, the generator renames it rather than dropping it. The taxonomy’s version arrives as VarVersion. If a property you expect is missing, look for a Var prefix before assuming the field isn’t there.
You may also see [Obsolete] warnings on Contract.Value. That is deliberate. value was an upload-time alias of amount — one period’s payment, never a total — and reading it as a contract’s worth understates the agreement. It is no longer written and now always reads null; use TotalValue instead. The attribute stays until the field itself goes in a future API version, because removing a documented response field from v1 would break clients that still reference it.
What else is there
The client covers the whole documented API — contracts, reviews, rulebooks, key dates, tags, search, insights, webhooks, conversions and jobs. Each group is its own *Api class under Clment.Sdk.Api. Browse the API reference for the full surface, or read Workflow examples for end-to-end recipes.
Choosing between the SDKs
- TypeScript — hand-written over generated types. Friendlier surface,
waitForhelpers, region as a typed argument. - .NET (this page) and Python — generated. Thinner, but exhaustive and guaranteed in step with the API.
All three are built from the same OpenAPI document, which you can also generate a client from in any other language — see Developers.