# KLCT developer documentation KLCT is AI customer support and growth automation. This is the complete developer documentation in one file. The machine-readable API description is at https://app.klctai.com/developers/openapi.json. # Introduction KLCT is AI customer support and growth automation. Your account holds contacts, support tickets, live chat, calls, a customer portal with a changelog and feedback board, and the campaigns and sequences that act on them. This API gives another product the same access a teammate has, limited to what you grant. There are three ways in, and they share one account model: | Need | Use | |---|---| | Read and write your own account's data from a script or server | [REST API](authentication) with an **account API key** | | Let your product connect to many customers' KLCT accounts | [REST API](oauth) with **OAuth 2.1** | | Let an AI assistant such as Claude work inside an account | The [MCP connector](mcp) | Everything is account-scoped. A token can only ever see the one KLCT account it belongs to, and asking for another account's record answers `404`, never `403`, so ids do not leak. ## Base URL and format ``` https://app.klctai.com/api ``` - Requests and responses are JSON. Send `Content-Type: application/json` on writes. - Timestamps are ISO 8601 in UTC. - Ids are integers. They are stable and never reused. - Lists are paginated with `page` and `per_page` (default 50, maximum 100) and return a `meta` block with `current_page`, `last_page` and `total`. - There is no version prefix today. Fields are only ever added. Treat unknown response fields as normal and ignore them. ## Make your first call Generate a key (see [Authentication](authentication)), then: ```bash curl "https://app.klctai.com/api/platform/account" \ -H "Authorization: Bearer $KLCT_API_KEY" ``` ```json { "account_id": 4821, "name": "Acme Dental", "billing_active": true, "setup_url": null } ``` Then list your newest contacts: ```bash curl "https://app.klctai.com/api/contacts?per_page=5" \ -H "Authorization: Bearer $KLCT_API_KEY" ``` ## What you can build - Create a support ticket or reply to a chat from your own app: [Tickets](reference#tickets), [Chat conversations](reference#chat-conversations). - Keep contacts in step with your CRM or store: [Contacts](reference#contacts). - Publish release notes from your pipeline to the customer portal: [Changelog](reference#changelog). - Pull feedback votes and the roadmap into your own tools: [Feedback and roadmap](reference#feedback-and-roadmap). - React in real time when a ticket, message or contact arrives: [Webhooks](webhooks). - Start a KLCT sequence when something happens in your product: [Events](reference#events-and-webhooks). ## For AI tools Every page here is also available as Markdown (add `.md` to the URL), the whole site is one file at `/developers/llms-full.txt`, and the API is described by an [OpenAPI 3.1 file](openapi.json) whose operation ids and descriptions are written to be used as tool definitions. See [Use with AI and MCP](mcp). # Authentication Every request carries a bearer token: ``` Authorization: Bearer ``` There are two kinds of token. Both are tied to exactly one KLCT account. ## Account API key The simplest option, for your own scripts and servers. 1. Sign in to KLCT as the account's main admin. 2. Open your **Profile** page and find the **API key** section. 3. Generate a key and store it somewhere safe. It is shown once. An account API key can use every endpoint that is not marked first-party. It carries all scopes, so treat it like a password. Regenerating the key revokes the old one at once. ```bash export KLCT_API_KEY="12|aBcDeF..." curl "https://app.klctai.com/api/platform/account" -H "Authorization: Bearer $KLCT_API_KEY" ``` Two things an account key cannot do: manage webhook subscriptions (those belong to an OAuth connection) and call first-party endpoints that use the shared machine token. ## OAuth access token For a product that connects to other people's KLCT accounts. The customer approves exactly the scopes they are comfortable with and can revoke the connection at any time. See [OAuth](oauth). ## Scopes Scopes narrow what an OAuth token can do. A request without the scope it needs is refused with `403`: ```json { "error": "insufficient_scope", "error_description": "This token does not have the \"tickets:write\" scope." } ``` | Scope | Grants | |---|---| | `identity` | Confirm which KLCT account this is. No data access by itself — just proves the connection belongs to one specific KLCT account. | | `contacts:read` | Read contacts. List and look up this account's contacts. | | `contacts:write` | Create and update contacts. Add new contacts and edit existing ones. Never deletes a contact. | | `calls:read` | Read call activity. List calls made or received by this account, with outcome and duration. | | `tickets:read` | Read support tickets. List and open this account's support tickets and the customer-visible conversation on each. Internal notes are never shown. | | `tickets:write` | Create and update support tickets. Open tickets, change their status, priority or due date, add internal notes and send replies to the customer. Never deletes a ticket. | | `conversations:read` | Read chat conversations. List and open this account's live chat conversations and their messages. Internal notes are never shown. | | `conversations:write` | Reply to and resolve chat conversations. Send a team reply into a chat conversation, which hands it to a person, and mark a conversation resolved. | | `changelog:read` | Read the portal changelog. List and open this account's changelog entries, drafts included. | | `changelog:write` | Write the portal changelog. Create and edit changelog entries, and publish them to the customer portal. Never deletes an entry. | | `feedback:read` | Read feedback and the roadmap. List customer feedback posts with their votes and status, and see the public roadmap. Customer names and emails are never included. | | `branding:write` | Set your brand (logo, colours, font). Update this account's logo, colour palette, website and typeface so KLCT emails, pages and social images match the connected platform. Never reads or deletes anything else. | | `sequences:trigger` | Trigger sequences from platform events. Forward an event (order placed, cart abandoned, etc.) to enroll a matching contact in a KLCT sequence. | | `webhooks:manage` | Manage its own webhook subscriptions. Register and remove URLs that KLCT calls when an event happens in this account. | `identity` is always granted. Account API keys pass every scope check. ## Keep tokens safe - Never put a token in browser code, a mobile app bundle or a public repository. Call the API from your server. - Use one key per integration so you can rotate them independently. Generating a new account key revokes the previous one. - Access tokens from OAuth expire after one hour. Use the refresh token to get a new one. - If a token leaks, revoke it: regenerate the account key, or disconnect the app under Integrations in KLCT. # OAuth 2.1 Use OAuth when your product connects to your customers' KLCT accounts. It is the standard authorization code flow with PKCE. The customer signs in to KLCT, picks the account to connect and ticks the scopes they allow. You never see their password. ## Get a client Clients are pre-registered; there is no open self-registration. Ask KLCT to register your product with: - a name shown on the consent screen, - one or more redirect URIs (HTTPS, or a loopback address for local testing; exact match, or a wildcard host such as `https://*.example.com/callback`), - the scopes you may ever request. You receive a `client_id` and a `client_secret`. ## 1. Send the customer to KLCT ``` GET https://app.klctai.com/oauth/platform/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.example/klct/callback &scope=contacts:read%20tickets:read%20tickets:write &state=RANDOM_STATE &code_challenge=BASE64URL_SHA256_OF_VERIFIER &code_challenge_method=S256 ``` PKCE with `S256` is required. Generate a random `code_verifier` (43 to 128 URL-safe characters) per attempt, keep it server-side, and send its SHA-256 hash, base64url-encoded, as `code_challenge`. Always check that the `state` you get back matches the one you sent. KLCT redirects to your `redirect_uri` with `?code=...&state=...`, or `?error=access_denied&state=...` if the customer declined. The code is valid for 10 minutes and works once. ## 2. Exchange the code ```bash curl -X POST "https://app.klctai.com/oauth/platform/token" \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d grant_type=authorization_code \ -d code=THE_CODE \ -d code_verifier=YOUR_VERIFIER \ -d redirect_uri=https://yourapp.example/klct/callback ``` You may send `client_id` and `client_secret` in the body instead of HTTP Basic. ```json { "access_token": "31|xYz...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "def50200...", "scope": "identity contacts:read tickets:read tickets:write" } ``` Check `scope`: it lists what the customer actually allowed, which can be less than you asked for. Then call `GET /platform/account` to learn which account you are connected to. ## 3. Refresh Access tokens last one hour. Refresh tokens last 30 days and **rotate on every use**: each refresh response contains a new refresh token and the old one stops working. Store the new one every time. ```bash curl -X POST "https://app.klctai.com/oauth/platform/token" \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token=THE_REFRESH_TOKEN ``` A refresh cannot add scopes. To get more, send the customer through step 1 again with the larger `scope`. ## Revoke ```bash curl -X POST "https://app.klctai.com/oauth/platform/revoke" \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d token=THE_ACCESS_OR_REFRESH_TOKEN ``` Customers can also disconnect your app from Integrations in KLCT. After that every request with that connection's tokens answers `401`. ## Errors Token endpoint errors follow RFC 6749: `invalid_request`, `invalid_client` (401), `invalid_grant` (expired or reused code, PKCE mismatch, revoked connection), `invalid_scope`, `unsupported_grant_type`. Each comes with an `error_description`. # Webhooks Webhooks tell your server the moment something happens in a KLCT account, so you do not have to poll. KLCT sends an HTTPS `POST` with a JSON body to a URL you register. Webhook subscriptions belong to an [OAuth connection](oauth), so they need the `webhooks:manage` scope. An account API key cannot create them. ## Subscribe ```bash curl -X POST "https://app.klctai.com/api/oauth-platform/webhooks" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"event_type":"ticket.created","target_url":"https://yourapp.example/hooks/klct"}' ``` The response includes a `signing_secret`. **It is shown once.** Store it to verify deliveries. The `target_url` must be HTTPS. Supported events: `contact.created`, `call.completed`, `ticket.created`, `conversation.message_received`, `changelog.published`, `feedback.created` You can list your subscriptions with `GET /oauth-platform/webhooks` and remove one with `DELETE /oauth-platform/webhooks/{id}`. Subscriptions end when the customer disconnects your app. ## What a delivery looks like ``` POST https://yourapp.example/hooks/klct Content-Type: application/json X-KLCT-Event: ticket.created X-KLCT-Signature: 7c1f...e90a ``` ```json { "event": "ticket.created", "id": "9b6e3f0c-7a1d-4f4e-9d52-2c1f0a6d8e11", "created_at": "2026-10-14T09:12:01+00:00", "data": { "ticket_id": 1042, "account_id": 4821, "subject": "Cannot log in on mobile" } } ``` `id` is unique per delivery. `data` depends on the event and is listed below. ## Verify the signature `X-KLCT-Signature` is the hex HMAC-SHA256 of the **raw request body** using your `signing_secret`. Compute it over the exact bytes you received, before parsing the JSON, and compare in constant time. Reject anything that does not match. ```js import { createHmac, timingSafeEqual } from 'node:crypto'; function verify(rawBody, header, secret) { const expected = createHmac('sha256', secret).update(rawBody).digest('hex'); const a = Buffer.from(expected), b = Buffer.from(header || ''); return a.length === b.length && timingSafeEqual(a, b); } ``` ```php $expected = hash_hmac('sha256', $request->getContent(), $signingSecret); abort_unless(hash_equals($expected, (string) $request->header('X-KLCT-Signature')), 401); ``` ## Delivery behaviour - Answer with any `2xx` within 10 seconds. - If the connection fails (timeout, DNS, refused), KLCT retries up to three attempts in total, waiting about 10 seconds and then about a minute. - A non-2xx answer is **not** retried; it is treated as your server's decision. Return `2xx` and process asynchronously if you need more time. - Deliveries can repeat and can arrive out of order. De-duplicate on `id`, and if order matters, fetch the current record with the API using the id in `data`. - Payloads are small on purpose. They carry ids and a few key fields; call the API for the rest. - Customer-visible data only: internal notes and staff details are never sent. ## Events ### contact.created A contact was added to the account, by any route. ```json { "contact_id": 90412, "name": "Dana Cole", "email": "dana@acme.example", "phone": "+15551234567", "lead_status": "new" } ``` ### call.completed An AI call finished. ```json { "phone_call_id": 55120, "contact_id": 90412, "from": "+15559876543", "to": "+15551234567", "duration": 142, "ended_reason": "customer-ended-call" } ``` ### ticket.created A support ticket was created, from email, chat, a form or the API. ```json { "ticket_id": 1042, "account_id": 4821, "ticket_number": "SUP-1042", "subject": "Cannot log in on mobile", "status": "open", "priority": 2, "source": "email", "contact_id": 90412, "created_at": "2026-10-14T09:12:00+00:00" } ``` Fetch the full ticket with `GET /tickets/{ticket_id}` (scope `tickets:read`). ### conversation.message_received A customer wrote in a chat conversation. Replies from your team or the AI, and internal notes, do not fire this. ```json { "conversation_id": 7781, "message_id": 20933, "account_id": 4821, "contact_id": 90412, "channel": "web", "sender_type": "visitor", "body": "Do you open on Sundays?", "created_at": "2026-10-14T09:18:00+00:00" } ``` `body` is cut to 2000 characters. Fetch the thread with `GET /conversations/{conversation_id}` (scope `conversations:read`) and answer with `POST /conversations/{id}/messages`. ### changelog.published A changelog entry went live: created as published, or moved from draft to published. Later edits do not fire it again. ```json { "entry_id": 214, "account_id": 4821, "title": "Faster ticket search", "version": "v2.4.0", "slug": "faster-ticket-search", "published_at": "2026-10-14T12:00:00+00:00" } ``` ### feedback.created A customer posted on the feedback board. The author's name and email are not included. ```json { "post_id": 88, "account_id": 4821, "title": "Dark mode", "status": "under_review", "category_id": 2, "created_at": "2026-10-14T08:30:00+00:00" } ``` # Errors and limits ## Status codes | Code | Meaning | |---|---| | 200, 201 | Success. Creating something answers 201 where noted. | | 401 | The token is missing, malformed, expired or revoked. | | 403 | The token is valid but lacks the scope, or the account has the module switched off (`error` explains which). | | 404 | No such record **in your account**. Another account's id looks the same, on purpose. | | 422 | Validation failed. | | 429 | Rate limit reached. Wait for the `Retry-After` header. | | 500 | Something broke on our side. Safe to retry reads; check before retrying a write. | ## Error body Most errors are small: ```json { "error": "Please provide valid contact data" } ``` Scope errors name the scope: ```json { "error": "insufficient_scope", "error_description": "This token does not have the \"contacts:write\" scope." } ``` Endpoints that use standard Laravel validation answer 422 with every failing field: ```json { "message": "The subject field is required.", "errors": { "subject": ["The subject field is required."] } } ``` Handle all three shapes: read `error`, then `message`, and use `errors` for field-level display. ## Rate limits | Surface | Limit | |---|---| | Account-scoped API | 60 requests per minute per token | | `POST /platform/contacts/sync` | 30 per minute per token, on top of the above | | MCP tool calls | 120 per minute per connection | | OAuth token endpoint | per IP, see `Retry-After` | Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. On `429`, wait for `Retry-After` seconds, then retry with a growing delay. Prefer the batch endpoints (`/platform/contacts/sync` takes 200 contacts a call) over many single calls. ## Idempotency and retries - `GET` is always safe to retry. - Writes are not deduplicated by KLCT. If a `POST` times out, read the resource back before sending it again so you do not create a duplicate ticket or contact. - Webhook deliveries can arrive more than once. De-duplicate on the delivery `id`. See [Webhooks](webhooks). ## Data you can and cannot change The API creates and updates. It does not hard-delete customer data (contacts, tickets, conversations). Removing records stays a decision a person makes inside KLCT. # Embeds and public endpoints These work from a visitor's browser, so they need no token. Each is created inside KLCT and identified by a slug you copy from its settings page. ## Support chat widget Create a widget under **Support, Widgets**, list your site under its allowed origins, and paste the loader on every page: ```html ``` The loader adds a chat bubble. Control it from your page with `window.klctSupport`: ```js klctSupport('open'); // open the chat klctSupport('close'); klctSupport('toggle'); klctSupport('sendMessage', 'I need help with my order'); klctSupport('book'); // open the booking window, if the widget has one klctSupport('dismissible'); // add a small hide cross to the bubble klctSupport('on', { event: 'open', cb: () => console.log('chat opened') }); // also: close, dismiss, booking:open, booking:close klctSupport('logout'); // call on sign-out so a shared computer forgets the visitor ``` ### Tell the widget who is signed in Call `identify` after your user signs in. Extra keys become visitor attributes the team sees next to the conversation. ```js klctSupport('identify', { external_id: 'user_8841', signature: 'HEX_HMAC_FROM_YOUR_SERVER', name: 'Dana Cole', email: 'dana@acme.example', plan: 'pro', }); ``` On its own `external_id` is only a label, because page code can claim anything. To make it a proven identity (so the same person is recognized across devices), compute the signature on **your server** and pass it down with the page: ```php $signature = hash_hmac('sha256', (string) $user->id, $widgetIdentitySecret); ``` ```js const signature = createHmac('sha256', process.env.KLCT_IDENTITY_SECRET).update(String(user.id)).digest('hex'); ``` The identity secret is on the widget's settings page. Never ship it to the browser. Turn on **Require verified identity** on the widget to refuse unsigned claims. ### Standalone ticket form A separate page for customers who prefer a form to a chat: ```html ``` Or link to `https://app.klctai.com/support/YOUR-WIDGET-SLUG/form`. ## Forms Build a form under **Forms**. It is available at: - `https://app.klctai.com/forms/YOUR-FORM-SLUG` as a hosted page, - `https://app.klctai.com/forms/YOUR-FORM-SLUG/embed.js` as a script that adds it to your page. Submissions create or update a contact, run the form's automations and carry ad and UTM attribution. To post a submission from your own server instead of the embed, send `POST https://app.klctai.com/forms/YOUR-FORM-SLUG/submit` with the `X-Form-Key` header from the form's settings. ## Booking Meeting types are bookable at `https://app.klctai.com/book/YOUR-HANDLE/EVENT-SLUG`, and each page has an `embed.js` at `https://app.klctai.com/book/YOUR-HANDLE/EVENT-SLUG/embed.js`. Your handle and event slugs are on the Booking pages. Visitors can cancel or reschedule from the links in their confirmation email. ## Landing pages Pages built in the page builder are served at `https://app.klctai.com/go/YOUR-PAGE-SLUG`, or on your own domain when you connect one. ## Customer portal The changelog, feedback board and roadmap you manage under **Portal** are public pages for your customers. To read or write the same data from code, use the [changelog](reference#changelog) and [feedback](reference#feedback-and-roadmap) endpoints. # Use with AI and MCP KLCT works with AI tools in three ways. Pick by what you want the AI to do. | You want to | Use | |---|---| | Have Claude draft emails, landing pages, forms, knowledge base articles, sequences and social posts inside your account | The **MCP connector** (below) | | Give your own agent or an MCP server generator tools for tickets, chat, contacts and the changelog | The **OpenAPI file** (below) | | Let a model read these docs | **llms.txt** and Markdown pages (below) | ## The MCP connector KLCT is a remote MCP server. Add it to Claude as a custom connector and write prompts like "draft a three-email welcome sequence in our brand voice" or "write a help article from this ticket thread". What Claude writes lands in your account as a draft that a person reviews. ``` https://app.klctai.com/mcp ``` - **Transport:** Streamable HTTP, `POST` to the URL above. - **Authorization:** OAuth 2.1 with PKCE. In Claude: Settings, Connectors, Add custom connector, paste the URL, choose **Sign in now**. KLCT shows a consent page where you pick the account and the scopes. Discovery documents are at `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`. - **Protocol:** serves the current MCP revision and earlier ones on the same endpoint. - **Not for data access.** The connector is deliberately limited to creative work. It cannot read contacts, conversations, call logs or billing, and it has **no delete, send or publish tools**. Forms are created inactive, landing pages stay drafts, knowledge base articles stay unpublished, sequences are created switched off. A person switches things on inside KLCT. - **Guardrails on every call:** the connecting person's own permissions are re-checked, the account's feature switches apply, there is a daily cap on writes per account, and each call is logged. ### Tools (63) This list is generated from the live tool registry, so it is always current. Full JSON input schemas are at [`/developers/mcp-tools.json`](mcp-tools.json). - `get_brand_kit` (read only, scope `content:read`): Returns the account's business name, website, logos, colour palette, brand voice and content guidelines. Call this before drafting anything so the copy and design match the brand. - `list_merge_tags` (read only, scope `content:read`): Lists the merge tags (written as {{tag_name}}) that email and SMS templates can use, such as the contact's first name or the business phone number. Use exactly these names; anything else is sent literally. - `set_merge_tag` (writes a draft, scope `merge_tags:write`): Creates or updates one of the account's custom merge tags, usable in templates as {{key}}. Keys are lowercase letters, numbers and underscores. Built-in tags (see list_merge_tags) cannot be changed. Account admins only. - `list_email_templates` (read only, scope `content:read`): Lists the account's email templates (id, name, subject, type, tags) without their bodies. Optional search by name or subject. Use get_email_template to read a body. - `get_email_template` (read only, scope `content:read`): Returns one email template in full, including its HTML body, so it can be reviewed or revised with update_email_template. - `create_email_template` (writes a draft, scope `email_templates:write`): Creates a new email template in the account's library. Nothing is sent: the template only becomes an email when a person uses it in a sequence or campaign. Body is HTML; scripts and event handlers are stripped. Use merge tags from list_merge_tags for personalisation and always include {{unsubscribe_link}} in marketing templates. - `update_email_template` (writes a draft, scope `email_templates:write`): Revises an existing email template. Only the fields given are changed. Read it first with get_email_template. Sequences that use the template will send the new version from their next send, so say so when you report back. - `duplicate_email_template` (writes a draft, scope `email_templates:write`): Copies an existing email template into a new one so it can be revised without touching the original. Same as the library's Duplicate action. - `preview_email` (read only, scope `content:read`): Renders an email the way a recipient would see it: merge tags replaced with the account's real values and a sample contact (John Doe), list styles inlined, the unsubscribe footer appended. Pass a template id, or a subject and body_html to preview unsaved work. Nothing is stored or sent. - `send_test_email` (writes a draft, scope `test_emails:self`): Emails a rendered draft to the connected person's own KLCT email address (and nobody else) so they can check it in a real inbox. Pass a template id, or a subject and body_html. Limited to 10 a day; nothing else is ever sent by this connector. - `list_sms_templates` (read only, scope `content:read`): Lists the account's SMS templates with their full text (SMS bodies are short). Optional search by name or text. - `create_sms_template` (writes a draft, scope `sms_templates:write`): Creates an SMS template (plain text, up to 1000 characters; one segment is 160 GSM characters). Nothing is sent. Merge tags from list_merge_tags work here too. KLCT appends the opt-out footer at send time, so do not write one. - `update_sms_template` (writes a draft, scope `sms_templates:write`): Revises an existing SMS template's name or text. Only the fields given are changed. - `list_landing_pages` (read only, scope `content:read`): Lists the account's landing pages with their status (draft or published) and whether the draft has unpublished changes. Use get_landing_page to read a draft. - `get_landing_page` (read only, scope `content:read`): Returns one landing page's draft: HTML, CSS, design settings and SEO fields. This is the draft the builder edits, not necessarily what visitors currently see. - `create_landing_page` (writes a draft, scope `landing_pages:write`): Creates a new landing page as an unpublished draft with the given design. It is not visible to the public until a person publishes it in KLCT. Write the page body as HTML sections using the built-in classes: lp-section, lp-container, lp-row, lp-col, lp-grid (lp-grid-3, lp-grid-4), lp-eyebrow, lp-lead, lp-muted, lp-btn (lp-btn-lg, lp-btn-outline), lp-card, lp-testimonial, lp-stat, lp-divider, lp-spacer, lp-footer. Extra styling goes in css. To place one of the account's forms, add
using a slug from list_forms (only active forms render). No