For developers
Your leads in your systems, and an assistant to act on them.
A scoped REST API, signed webhooks and an MCP endpoint. One permission model, one audit trail, set up in Settings.
- Scoped keys, revocable instantly
- Every webhook signed
- Every request audited
curl https://clientturn.com/api/v1/me \
-H "Authorization: Bearer ct_live_your_key_here"{
"workspace": { "id": "…", "name": "Northwind Digital", "timezone": "Europe/London" },
"key": { "name": "CRM sync", "environment": "live",
"scopes": ["leads:read", "leads:write"] },
"acting_as": { "role": "admin" }
}Three ways in
Pull, be pushed to, or hand it to an assistant.
One set of permissions, one audit trail, one place in Settings.
REST API
Create, read and update leads over HTTPS. Scoped and rate limited per key, under the app's rules.
Read the referenceWebhooks
A signed JSON POST when a lead arrives, qualifies, books, replies or needs a person. Retried on failure.
See the eventsMCP
Connect Claude, Codex or Gemini with the same key. Anything risky waits for a person.
Connect an assistantQuickstart
Three minutes, no sales call.
The key is shown once; we store only its fingerprint.
Create a key
Settings → Developer → New key. Choose scopes, expiry and optional IP limits.
Send it as a bearer token
From your own server only. No CORS headers, so browsers cannot use it.
Check it with /me
Shows the workspace, scopes and live role, so you can see why a call is refused.
curl "https://clientturn.com/api/v1/leads?status=QUALIFIED&limit=25" \
-H "Authorization: Bearer ct_live_your_key_here"curl -X PATCH https://clientturn.com/api/v1/leads/{id} \
-H "Authorization: Bearer ct_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "status": "WON", "note": "Signed the proposal." }'API reference
Everything the app can do, through the same rules.
Each endpoint calls the same operation the app uses, so scoping, status rules and audit always apply.
| Endpoint | Scope | Role | What it does |
|---|---|---|---|
GET /api/v1/me | business:read | Viewer | The workspace, the key's scopes and its live role. Start here. |
GET /api/v1/leads | leads:read | Viewer | Search leads by text, status or attention flag. |
POST /api/v1/leads | leads:write | Member | Record an inbound lead through the ad-form intake. Needs an Idempotency-Key and a warm relationship. 201 created, 200 merged or duplicate, 409 if suppressed (nothing stored). |
GET /api/v1/leads/{id} | leads:read | Viewer | One lead, with its last 20 messages as recent_activity. |
PATCH /api/v1/leads/{id} | leads:write | Member | Change details, move status, add a note. Same status rules as the app. |
GET /api/v1/events | business:read | Viewer | Recent webhook deliveries: what was sent, your server's reply, and whether it will retry. |
Creating a lead uses the same intake
POST /api/v1/leads runs the same deduplication, suppression check and attribution as every other source. It records and qualifies the lead, but never starts follow-up; a person does that.
Permissions
A key can only narrow what its owner could already do.
Every request checks the key’s scopes and its owner’s current role; the narrower wins.
- The same scopes govern the API, webhooks and MCP
- Demote someone and their keys lose reach on the next call
- Keys expire in 90 days unless you choose otherwise
- No all-access scope, and nothing selected by default
| Scope | Grants |
|---|---|
leads:read | Read your leads and their conversations |
leads:write | Create and update leads |
prospects:read | Read sourced prospects and their scores |
prospects:write | Approve prospects and start sourcing |
campaigns:read | Read campaign performance |
campaigns:write | Change and launch campaigns |
analytics:read | Read your metrics |
business:read | Read your business profile and status |
business:write | Change connected systems and workspace configuration |
agents:read | See your AI agents, how they are set up and what they have done |
agents:write | Create and configure AI agents, and start, pause or stop them |
Webhooks
Told the moment it happens, provably by us.
Each POST carries an HMAC-SHA256 signature over the timestamp and raw body, so you can verify it and refuse replays.
{
"id": "8f14e45f-ceea-467a-9f5b-2c1e9d4a77b3",
"type": "lead.qualified",
"created_at": "2026-09-08T09:14:02.117Z",
"data": {
"lead_id": "3a7c…",
"result": "QUALIFIED",
"status": "QUALIFIED",
"reasons": [{ "code": "in_service_area" }]
}
}import crypto from "node:crypto";
// The RAW body, before JSON.parse. Re-serialising changes the key
// order and the signature will fail intermittently.
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
// Reject anything older than five minutes. This is what stops a
// request anyone once saw from being replayed forever.
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
// Constant time. A plain === leaks the signature a byte at a time.
return crypto.timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected),
);
}Events
Only events that something actually sends are listed.
| Event | Sent when |
|---|---|
lead.created | A new lead reached the workspace, from any source — an ad form, an import, or added by hand. |
lead.qualified | A lead finished qualification and was scored. Carries the outcome and the answers it was based on. |
lead.status_changed | A lead moved to a different status, by a person or by automation. |
lead.handover_required | Follow-up stopped and a human is needed — the usual trigger for paging someone in your own system. |
booking.created | An appointment was booked for a lead. |
message.received | An inbound message arrived from a lead on any channel. |
lead.touched | Someone who is already a lead arrived again through a form, an import or the API. The existing lead was matched and updated rather than duplicated. |
lead.scored | A lead received a new score: the total out of 100, the grade and what triggered the re-score. |
score.changed | A new score moved a lead to a different grade (A to D). Carries the previous and the new grade. |
reply.received | A lead replied on any channel. Fires once per inbound message, from the stored message itself. |
meeting.booked | A meeting was confirmed: a calendar accepted it, or a person confirmed a requested time. |
meeting.pending | A lead chose a time that no connected calendar could confirm. It waits for someone in the workspace to confirm or decline it. |
meeting.cancelled | A booked or requested meeting was cancelled, by the lead, a calendar or a person. |
meeting.no_show | A booked meeting was marked as a no-show. |
contact.unsubscribed | A person opted out, on one channel or all of them. Carries the channel so your systems can honour the same scope. |
contact.suppressed | An address or number was blocked for a reason other than an opt-out: a bounce, a complaint, an invalid number or a manual block. |
opportunity.created | A lead became a sales opportunity. Carries the lead, the starting stage and the sales motion. |
opportunity.won | An opportunity was closed as won. Carries the lead and the reason given, and the payment (amount, currency, recurring, interval, MRR) when a confirmed payment closed it. |
opportunity.lost | An opportunity was closed as lost. Carries the lead and the reason given. |
lead.intent_changed | A lead's buying intent moved to a new state (for example EXPLORATORY to BOOKING_READY, or to NOT_NOW). Carries the new and previous state, the intent score, the next best action and how complete qualification is. |
ai.escalated | The conversation assistant handed a lead to a person. Carries the reason and priority of the handover. |
quote.sent | A quote was sent to a customer. Carries the quote id and number, the opportunity and lead, the revision, the total and the valid-until date. |
quote.viewed | The customer opened a sent quote for the first time. Fires once per revision, not on every view. |
quote.accepted | The customer accepted a quote on its public page. Carries the quote id, number and revision. |
quote.declined | The customer declined a quote. Carries the quote id, number and revision. |
quote.expired | A sent quote passed its valid-until date without being accepted, and can no longer be signed. |
signature.completed | The customer signed a quote with a simple electronic signature. The signature record, with its document fingerprint, stays in ClientTurn. |
invoice.issued | An invoice was numbered and issued to the customer. Carries the invoice id and number, the total, the currency and the due date. |
invoice.paid | An invoice was paid in full. Carries the invoice id and number, the total and the quote it came from. |
invoice.overdue | An issued invoice passed its due date unpaid and its first overdue reminder went out. Carries the amount still due. |
Delivery
- Retries on failure7 attempts over ~23h
- A 4xx from your serverNot retried
- Repeated failureEndpoint paused, and you are told why
- Duplicate deliveriesStable event id, unique per endpoint
- Cannot expose an endpoint?Poll
/api/v1/events
Model Context Protocol
Give an assistant a key to the workspace, and a leash.
Point Claude, Codex or Gemini at ClientTurn’s MCP server with your API key; it acts only within the permissions you grant.
- Leads, conversations, appointments, campaigns, prospects and connections
- Sets up AI agents: sources, schedule and limits
- Tools it cannot use are not even listed to it
- Sending, launching or resuming a campaign, starting an agent, or disconnecting a system all wait for a person
- No model, prompt or token budget settings, for anyone
- Every call it makes, and every refusal, is in your audit log
- Revoke the key and it disconnects immediately
claude mcp add --transport http clientturn \
https://clientturn.com/api/mcp \
--header "Authorization: Bearer ct_live_your_key_here"{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "lead.set_status",
"arguments": { "leadId": "3a7c…", "status": "QUALIFIED" }
}
}The approval gate
High-impact requests park with a plain-English summary, and the assistant is told nothing has happened. Approval runs it once, on the approver’s authority.
AI agents
Set an agent up in a sentence, start it on purpose.
Running an agent spends money on a schedule, so starting one always waits for you.
Configured by conversation
The assistant sets type, cadence, sources, limits and review level. New agents start as drafts.
Started only by you
Starting or running now parks for approval. Agents cannot configure agents. Pausing is never gated.
The model stays ours
No one can set a model, temperature, token budget or system prompt, through any surface.
What we guarantee
Built so a leaked key is a small problem.
Properties of the code, each covered by a test that fails if it stops being true.
Shown once, stored as a digest
We keep only a SHA-256 fingerprint. No screen or support process can show the key again.
A key is one person's authority
Every request re-reads the owner's membership. Remove someone and their keys stop on the next call.
Scopes narrow, never widen
Write access on a viewer's authority still cannot write. Both checks run; the narrower wins.
Never usable from a browser
No CORS headers are sent, so a key pasted into front-end JavaScript will not work.
Every request is on the record
Allowed or refused, with the key that made it, visible to your admins in Settings.
Optional IP allowlist
List your fixed addresses. If the caller's address is unknown, the request is refused.
Errors
A stable code, a readable message, and an id to quote.
Refusals never say which check failed; your admins see the reason in Settings.
A multi-change PATCH is not a transaction. If a step fails, the response names what was already applied.
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | The key is missing, unknown, revoked, expired or blocked by its IP allowlist. |
forbidden | 403 | The key lacks the scope, or its owner no longer holds the role. |
invalid_request | 400 | The body or a parameter did not validate. The message names the field. |
not_found | 404 | No such record in this workspace. |
rate_limited | 429 | 300 requests a minute per key. `retry_after` says how long to wait. |
needs_confirmation | 409 | A person must agree, so the API cannot do it. `effect` says what would have happened. |
server_error | 500 | Our fault. Quote the `request_id`; it identifies the exact attempt. |
Questions
Before you build
Workspace owners and admins, in Settings → Developer. A key outlives the session that made it, so members cannot mint their own. It stops working on the next request once its owner leaves the workspace.
Yes. POST /api/v1/leads runs the same intake as the ad forms, with deduplication, the suppression check and attribution. It needs an Idempotency-Key and a warm relationship, and never starts follow-up; a person does that.
Some actions, such as archiving a lead or sending a message, need a person to agree. The API refuses rather than guessing, and returns what would have happened.
No. Poll GET /api/v1/events instead. It returns the same payloads, your server's replies and whether we will retry.
Same permission model over JSON-RPC, for AI assistants, with more reach: appointments, campaigns, prospects, connections, metrics and AI agents. Anything needing confirmation parks for a person to approve, and the assistant is told nothing has happened yet.
It can create an agent and set its sources, schedule and limits, but a new agent is a draft and starting one parks for your approval. Models, temperatures, token budgets and system prompts are not exposed to anyone, through any surface.
Any MCP client that can send a bearer header, including Claude, Codex and Gemini. Your workspace API key is the only credential.
No. Keys are labelled Live or Test so you can tell them apart, but both read the same workspace.
Start building
Create a key and make your first call today.
Everything here is on every plan, including the free trial. No integration tier and no per-call pricing at clientturn.com.
- Nothing charged during the trial
- Keys created in Settings, in seconds
- Revocable at any time
- REST APICreate, read and update leads from your own systems.
- Signed webhooksTold the moment something happens, provably by us.
- MCPAn AI assistant working your pipeline, within limits.
- One audit trailEvery call, every refusal, every approval on the record.
