67 REST routes for requests, planned items, engagements, cycles, clients, comments and time entries, read-only invoices and retainers, share links, bulk changes and the website widget. AI agents get a hosted MCP server. Server keys and MCP are on the Team and Agency plans and the 14-day trial; the widget works on every plan.
Two key types per workspace, both created in workspace settings and sent in the X-API-Key header. Server keys (prefixed kl_live_) act as the member who created them, limited to the scopes you grant and never beyond that member's role. Widget keys (kl_pub_) only create requests from the website widget, and only from the origins you allow. Keys created before October 2026 start with dvf_ and keep working.
Example: a law firm's client portal logs a matter task. The fields are the request fields; your industry template decides what they are called in the app, and the reference number uses your workspace's prefix.
curl -X POST https://api.klientele.io/api/public/v2/requests \
-H "X-API-Key: kl_live_••••••••" \
-H "Content-Type: application/json" \
-d '{
"clientId": "b3f1c9e0-2a4d-4e3a-9c1e-6d2a7f8e0b11",
"title": "Review the revised lease before Friday",
"description": "Landlord sent a new draft. Clause 7 now caps the break option at 12 months.",
"urgency": "high",
"reporterName": "Priya Shah",
"reporterEmail": "[email protected]",
"tags": ["lease", "deadline"]
}'{
"status": "success",
"data": {
"id": "6f0d2c1a-8e4b-4b7e-9a51-3c2d7e9f1a04",
"requestNumber": "TASK-2026-0042",
"status": "new",
"source": "api",
"createdAt": "2026-10-04T09:12:44.512Z"
}
}Files can travel with the create. Add an attachments array to a request, planned item or engagement: each entry is either { "fileName", "base64" } or { "url" } for a public https file we download for you. Up to 10 files; your workspace's allowed file types and size limit apply. One refused file refuses the whole call and nothing is created. Inline content counts towards the 10 MB request limit, so send larger files by URL. Attaching needs the key to hold attachment.create.
curl -X POST https://api.klientele.io/api/public/v2/requests \
-H "X-API-Key: kl_live_••••••••" \
-H "Content-Type: application/json" \
-d '{
"clientId": "b3f1c9e0-2a4d-4e3a-9c1e-6d2a7f8e0b11",
"title": "Countersign the lease",
"urgency": "high",
"attachments": [
{ "url": "https://files.example.com/lease-v3.pdf" },
{ "fileName": "notes.txt", "base64": "Q2xhdXNlIDcgY2hhbmdlZC4=" }
]
}'<script
src="https://api.klientele.io/widget/v1/klientele.js"
data-key="kl_pub_••••••••"
data-client-id="b3f1c9e0-2a4d-4e3a-9c1e-6d2a7f8e0b11"
defer
></script>Requests that come in through the widget, the API or the email inbox count towards your plan's monthly allowance of external requests.
67 routes, grouped by resource. Paths are relative to the base URL. Paths use the neutral names: requests, planned-items, engagements and cycles. Your industry template decides what each is called in the app.
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| GET | /whoami | The workspace, member, role and effective scopes a key acts with. Answers on every plan and says whether API access is on. | Server key · no scope needed |
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| POST | /requests | Create a request; optional attachments (base64 or https URL) in the same call | Server key · requests:create |
| GET | /requests | List requests; filter by state (open, finished or all; finished = done or cancelled), status, clientId, engagementId, cycleId; page and limit | Server key · requests:read |
| GET | /requests/:idOrNumber | Retrieve a request by id or reference number | Server key · requests:read |
| PATCH | /requests/:id | Update a request | Server key · requests:update |
| PATCH | /requests/:id/archive | Archive a request | Server key · requests:update |
| DELETE | /requests/:id | Delete a request | Server key · requests:delete |
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| POST | /planned-items | Create a planned item; optional attachments in the same call | Server key · planned-items:create |
| GET | /planned-items | List planned items; filter by state (open, finished or all; finished = done or declined), status, clientId; page and limit | Server key · planned-items:read |
| GET | /planned-items/:id | Retrieve a planned item | Server key · planned-items:read |
| PATCH | /planned-items/:id | Update a planned item | Server key · planned-items:update |
| PATCH | /planned-items/:id/archive | Archive a planned item | Server key · planned-items:update |
| DELETE | /planned-items/:id | Delete a planned item | Server key · planned-items:delete |
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| POST | /engagements | Create an engagement; optional attachments in the same call | Server key · engagements:create |
| GET | /engagements | List engagements | Server key · engagements:read |
| GET | /engagements/:id | Retrieve an engagement | Server key · engagements:read |
| PATCH | /engagements/:id | Update an engagement | Server key · engagements:update |
| PATCH | /engagements/:id/archive | Archive an engagement | Server key · engagements:update |
| DELETE | /engagements/:id | Delete an engagement | Server key · engagements:delete |
Cycles have no archive route. One can be deleted once nothing is left in it.
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| POST | /cycles | Create a cycle | Server key · cycles:create |
| GET | /cycles | List cycles | Server key · cycles:read |
| GET | /cycles/:id | Retrieve a cycle | Server key · cycles:read |
| PATCH | /cycles/:id | Update a cycle | Server key · cycles:update |
| DELETE | /cycles/:id | Delete an empty cycle | Server key · cycles:delete |
Deleting a client archives it. Add ?permanent=true to an archived client to remove it for good. A client holds several contacts, each with roles that decide which email they receive; create takes an optional primaryContact { name, email, phone, title }. Changed October 2026: contactEmail and contactPhone were removed from clients (requests that send them are refused); read and change the people at a client through /clients/:id/contacts.
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| POST | /clients | Create a client | Server key · clients:create |
| GET | /clients | List clients | Server key · clients:read |
| GET | /clients/:id | Retrieve a client | Server key · clients:read |
| PATCH | /clients/:id | Update a client | Server key · clients:update |
| DELETE | /clients/:id | Archive a client, or delete it permanently | Server key · clients:delete |
| GET | /clients/:id/contacts | List a client's contacts, contact roles and which email each role receives | Server key · client-contacts:read |
| POST | /clients/:id/contacts | Add a contact | Server key · client-contacts:create |
| PATCH | /clients/:id/contacts/:contactId | Edit a contact, or make it the primary contact | Server key · client-contacts:update |
| DELETE | /clients/:id/contacts/:contactId | Remove a contact | Server key · client-contacts:delete |
Body: content, and optionally clientVisible to share the comment with the client portal.
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| GET | /requests/:requestId/comments | List comments on a request | Server key · request-comments:read |
| POST | /requests/:requestId/comments | Add a comment to a request | Server key · request-comments:create |
| GET | /planned-items/:plannedItemId/comments | List comments on a planned item | Server key · planned-item-comments:read |
| POST | /planned-items/:plannedItemId/comments | Add a comment to a planned item | Server key · planned-item-comments:create |
Off when the workspace has turned time tracking off.
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| GET | /time-entries | List time entries | Server key · time-entries:read |
| POST | /time-entries | Log time: clientId, hours, description, billableItemType, and optionally an engagement, cycle, date and billable flag | Server key · time-entries:create |
Two steps: create a link, then PUT the raw file to it within 15 minutes. The link takes one upload; its size, sha256 (when given) and contents are checked, and nothing is stored on a mismatch.
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| POST | /attachment-uploads | Create a single-use upload link for one file on a request, planned item, engagement, invoice, retainer or expense: itemType, itemId, fileName, mimeType, sizeBytes and optionally sha256. Returns uploadUrl, expiresAt and the exact curl command | Server key · attachment-uploads:create |
| PUT | /uploads/:token | Send the file's raw bytes to the link (no other auth). Returns the stored attachment | The link itself |
| GET | /attachment-uploads/:uploadId | An upload's status, and its attachment once stored | Server key · attachment-uploads:read |
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| GET | /account/invoices | List invoices | Server key · invoices:read |
| GET | /account/invoices/:invoiceId | Retrieve an invoice | Server key · invoices:read |
| GET | /account/retainers | List retainers | Server key · retainers:read |
| GET | /account/retainers/:retainerId | Retrieve a retainer | Server key · retainers:read |
The token in the link is the credential: no API key, open to any origin. These back the invoice and retainer pages you email to clients, and only work while sharing is on for that document.
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| GET | /invoices/:token | View a shared invoice with its payments; marks it viewed on first open | Share token |
| POST | /invoices/:token/sign | Client signs the invoice (signedBy, optional signatureUrl) | Share token |
| GET | /retainers/:token | View a shared retainer agreement | Share token |
| POST | /retainers/:token/sign | Client signs the retainer agreement (signedBy, optional signatureUrl) | Share token |
Widget keys only work from the origins you allow on the key.
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| GET | /widget/config | Check the key, origin and plan before the widget renders | Widget key |
| POST | /widget/requests | Create a request from the widget | Widget key |
| POST | /widget/requests/:requestNumber/attachments | Attach up to 5 files (screenshot, recording) to that request | Widget key |
Up to 200 records per call. Bulk delete needs the matching :delete scope as well as :bulk, and refuses the whole batch if any record is not safe to delete.
| Method | Path | Purpose | Key · scope |
|---|---|---|---|
| PATCH | /requests/bulk | Update many requests | Server key · requests:bulk |
| POST | /requests/bulk-archive | Archive many requests | Server key · requests:bulk |
| DELETE | /requests/bulk | Delete many requests | Server key · requests:bulk + requests:delete |
| PATCH | /planned-items/bulk | Update many planned items | Server key · planned-items:bulk |
| POST | /planned-items/bulk-archive | Archive many planned items | Server key · planned-items:bulk |
| DELETE | /planned-items/bulk | Delete many planned items | Server key · planned-items:bulk + planned-items:delete |
| PATCH | /engagements/bulk | Update many engagements | Server key · engagements:bulk |
| POST | /engagements/bulk-archive | Archive many engagements | Server key · engagements:bulk |
| DELETE | /engagements/bulk | Delete many engagements | Server key · engagements:bulk + engagements:delete |
| PATCH | /cycles/bulk | Update many cycles | Server key · cycles:bulk |
| POST | /cycles/bulk-archive | Archive many cycles | Server key · cycles:bulk |
| DELETE | /cycles/bulk | Delete many cycles | Server key · cycles:bulk + cycles:delete |
| PATCH | /clients/bulk | Update many clients | Server key · clients:bulk |
| DELETE | /clients/bulk | Delete many clients | Server key · clients:bulk + clients:delete |
Outbound webhooks are set up in workspace settings, not through this API. Each delivery is signed with HMAC-SHA256 in the X-Klientele-Signature header.
Klientele runs a hosted MCP server at https://mcp.klientele.io/mcp. Nothing to install. It is on the Team and Agency plans and the 14-day trial.
Most MCP clients, Claude included, only need the URL: add it, sign in to Klientele and choose what the app may do: read only, read and write, or full access. The agent acts as you and can never do more than your role allows. A workspace admin can switch API access off for everyone. Using Claude? Follow Connect Klientele to Claude.
The server offers more than 200 tools across requests, planned items, engagements, cycles, clients, comments, attachments, time and timers, deliverables and milestones, expenses and their receipts, retainers, invoices, payments and credit notes, plus reports and notifications. Five of them open interactive views in Claude. Tool titles use your workspace's own words, so a law firm sees matter tasks where a software team sees tickets. It does not administer the workspace: roles, API keys, webhooks, settings, import, export and team invitations stay in the app.
Sign-in uses OAuth 2.1 with dynamic client registration and PKCE. Claude, Cursor and OpenAI Codex all support it. A client that can only use a pre-registered client ID cannot sign in yet; give it a server key instead.
claude mcp add --transport http klientele https://mcp.klientele.io/mcp{
"mcpServers": {
"klientele": { "url": "https://mcp.klientele.io/mcp" }
}
}[mcp_servers.klientele]
url = "https://mcp.klientele.io/mcp"Scripts, CI jobs and clients that cannot sign in send a server key as a bearer token instead. The key acts as the member who created it, with the permissions ticked when it was made. Keep it in an environment variable:
claude mcp add --transport http klientele https://mcp.klientele.io/mcp \
--header "Authorization: Bearer ${KLIENTELE_API_KEY}"[mcp_servers.klientele]
url = "https://mcp.klientele.io/mcp"
bearer_token_env_var = "KLIENTELE_API_KEY"Server keys: 120 requests per minute per key. Widget keys: 30 requests per minute per key and origin. Limits reset every 60 seconds.
When you exceed the limit, you receive a 429 Too Many Requests response with a Retry-After header. If you need more throughput, talk to us.
Send an Idempotency-Key header on any POST, PATCH or DELETE: 8 to 200 characters of letters, digits and . _ : -, new for each change you mean to make. If the connection drops, send the same request with the same key. For 24 hours, a retry that matches the original gets the original response back with Idempotent-Replayed: true, and nothing happens twice. Keys belong to the member the key acts as.
curl -X POST https://api.klientele.io/api/public/v2/requests \
-H "X-API-Key: kl_live_••••••••" \
-H "Idempotency-Key: 01JB9Z6Q8W7X3K5M2N4P6R8T0V" \
-H "Content-Type: application/json" \
-d '{ "clientId": "b3f1c9e0-2a4d-4e3a-9c1e-6d2a7f8e0b11", "title": "Review the lease", "urgency": "high" }'A match means the same method, path, query and body; files count by their content. Any answer other than a success frees the key, so you can fix the request and send it again with the same key. The ways a key is refused:
409 IDEMPOTENCY_CONFLICT: the key was already used for a different request. Use a new key.409 IDEMPOTENCY_IN_PROGRESS: the first attempt is still running. Wait for the Retry-After seconds and send it again.409 IDEMPOTENCY_RESPONSE_UNAVAILABLE: the first attempt worked, but its response was over 1 MB, too large to keep. Read the result instead.400 IDEMPOTENCY_KEY_INVALID: the key is not 8 to 200 of the allowed characters.The MCP server sends a key with every tool that writes, so an agent that repeats a call after a timeout does not repeat the change.
Every request and planned item carries a version. Send the version you read with your update. If someone changed the item since, the update is refused with409 EDIT_CONFLICT and nothing is written. Without aversion, the update is not checked against what you read.
curl -X PATCH https://api.klientele.io/api/public/v2/requests/7d0c…e21 \
-H "X-API-Key: kl_live_••••••••" \
-H "Content-Type: application/json" \
-d '{ "urgency": "critical", "version": 4 }'The conflict tells you what to do next:
fields: every field changed since your version, withtheirs (stored now), mine (what you sent) andoverlap (you both changed it, to different values).current: the item exactly as a GET returns it, andcurrentVersion. Apply your change on top and send it again with that version.changedBy and changedAt: who made the change and when.Sending the same values twice is safe: an update whose values already match what is stored succeeds and changes nothing. Bulk updates take versions (one per id) and refuse the whole batch if any of them is stale. The MCP server sends the version an agent last read for you.
Start a trial, create a server key in workspace settings, and make your first call.