Skip to content

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
Check a key works
curl https://clientturn.com/api/v1/me \
  -H "Authorization: Bearer ct_live_your_key_here"
Response
{
  "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 reference

Webhooks

A signed JSON POST when a lead arrives, qualifies, books, replies or needs a person. Retried on failure.

See the events

MCP

Connect Claude, Codex or Gemini with the same key. Anything risky waits for a person.

Connect an assistant

Quickstart

Three minutes, no sales call.

The key is shown once; we store only its fingerprint.

  1. Create a key

    Settings → Developer → New key. Choose scopes, expiry and optional IP limits.

  2. Send it as a bearer token

    From your own server only. No CORS headers, so browsers cannot use it.

  3. Check it with /me

    Shows the workspace, scopes and live role, so you can see why a call is refused.

List qualified leads
curl "https://clientturn.com/api/v1/leads?status=QUALIFIED&limit=25" \
  -H "Authorization: Bearer ct_live_your_key_here"
Update a lead
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.

ClientTurn API endpoints, with the scope and minimum workspace role each requires
EndpointScopeRoleWhat it does
GET /api/v1/mebusiness:readViewerThe workspace, the key's scopes and its live role. Start here.
GET /api/v1/leadsleads:readViewerSearch leads by text, status or attention flag.
POST /api/v1/leadsleads:writeMemberRecord 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:readViewerOne lead, with its last 20 messages as recent_activity.
PATCH /api/v1/leads/{id}leads:writeMemberChange details, move status, add a note. Same status rules as the app.
GET /api/v1/eventsbusiness:readViewerRecent 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
The permissions a ClientTurn API key or assistant connection can be granted
ScopeGrants
leads:readRead your leads and their conversations
leads:writeCreate and update leads
prospects:readRead sourced prospects and their scores
prospects:writeApprove prospects and start sourcing
campaigns:readRead campaign performance
campaigns:writeChange and launch campaigns
analytics:readRead your metrics
business:readRead your business profile and status
business:writeChange connected systems and workspace configuration
agents:readSee your AI agents, how they are set up and what they have done
agents:writeCreate 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.

What arrives
{
  "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" }]
  }
}
Verifying it (Node)
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.

The events ClientTurn will send to your endpoint
EventSent when
lead.createdA new lead reached the workspace, from any source — an ad form, an import, or added by hand.
lead.qualifiedA lead finished qualification and was scored. Carries the outcome and the answers it was based on.
lead.status_changedA lead moved to a different status, by a person or by automation.
lead.handover_requiredFollow-up stopped and a human is needed — the usual trigger for paging someone in your own system.
booking.createdAn appointment was booked for a lead.
message.receivedAn inbound message arrived from a lead on any channel.
lead.touchedSomeone 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.scoredA lead received a new score: the total out of 100, the grade and what triggered the re-score.
score.changedA new score moved a lead to a different grade (A to D). Carries the previous and the new grade.
reply.receivedA lead replied on any channel. Fires once per inbound message, from the stored message itself.
meeting.bookedA meeting was confirmed: a calendar accepted it, or a person confirmed a requested time.
meeting.pendingA lead chose a time that no connected calendar could confirm. It waits for someone in the workspace to confirm or decline it.
meeting.cancelledA booked or requested meeting was cancelled, by the lead, a calendar or a person.
meeting.no_showA booked meeting was marked as a no-show.
contact.unsubscribedA person opted out, on one channel or all of them. Carries the channel so your systems can honour the same scope.
contact.suppressedAn 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.createdA lead became a sales opportunity. Carries the lead, the starting stage and the sales motion.
opportunity.wonAn 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.lostAn opportunity was closed as lost. Carries the lead and the reason given.
lead.intent_changedA 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.escalatedThe conversation assistant handed a lead to a person. Carries the reason and priority of the handover.
quote.sentA 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.viewedThe customer opened a sent quote for the first time. Fires once per revision, not on every view.
quote.acceptedThe customer accepted a quote on its public page. Carries the quote id, number and revision.
quote.declinedThe customer declined a quote. Carries the quote id, number and revision.
quote.expiredA sent quote passed its valid-until date without being accepted, and can no longer be signed.
signature.completedThe customer signed a quote with a simple electronic signature. The signature record, with its document fingerprint, stays in ClientTurn.
invoice.issuedAn invoice was numbered and issued to the customer. Carries the invoice id and number, the total, the currency and the due date.
invoice.paidAn invoice was paid in full. Carries the invoice id and number, the total and the quote it came from.
invoice.overdueAn 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
Connect Claude Code
claude mcp add --transport http clientturn \
  https://clientturn.com/api/mcp \
  --header "Authorization: Bearer ct_live_your_key_here"
Or speak JSON-RPC directly
{ "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.

ClientTurn API error codes and their meanings
CodeHTTPMeaning
unauthorized401The key is missing, unknown, revoked, expired or blocked by its IP allowlist.
forbidden403The key lacks the scope, or its owner no longer holds the role.
invalid_request400The body or a parameter did not validate. The message names the field.
not_found404No such record in this workspace.
rate_limited429300 requests a minute per key. `retry_after` says how long to wait.
needs_confirmation409A person must agree, so the API cannot do it. `effect` says what would have happened.
server_error500Our fault. Quote the `request_id`; it identifies the exact attempt.

Questions

Before you build

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.