API reference
Base URL https://app.klctai.com/api. Send Authorization: Bearer <token> on every request. The scope on each endpoint is the OAuth scope an access token needs; an account API key passes them all. Machine-readable: openapi.json or Markdown.
Account
Identify the account a token belongs to and keep its brand in step with your product.
GET /platform/account identity Get the connected account
Returns which KLCT account the token belongs to and whether billing is active. Call this first after connecting to confirm you hold the right account.
Operation id getAccount
Example
curl "https://app.klctai.com/api/platform/account" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"account_id": 4821,
"name": "Acme Dental",
"billing_active": true,
"setup_url": null
}Response fields
| Field | Type | Description |
|---|---|---|
account_id |
integer | The KLCT account id. |
name |
string | Account name. |
billing_active |
boolean | False when the account has no active plan yet. |
setup_url |
string | Where a person can finish setup when billing is not active, otherwise null. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "identity" scope (error: insufficient_scope).
- 429 Rate limit reached. Retry after the Retry-After header.
PUT /platform/branding branding:write Set the account brand
Updates the logo, colors, website and typeface KLCT uses in emails, pages and social images. Send only the fields you want to change. Never reads or deletes anything else.
Operation id updateBranding
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
primary_color |
string | Hex color, for example #1D4ED8. | |
secondary_color |
string | Hex color. | |
accent_color |
string | Hex color. | |
button_color |
string | Hex color for buttons. | |
button_text_color |
string | Hex color for button text. | |
logo_url |
string | HTTPS URL of the logo. | |
email_logo_url |
string | HTTPS URL of the logo used in email headers. | |
square_logo_url |
string | HTTPS URL of a square logo. | |
favicon_url |
string | HTTPS URL of the favicon. | |
website_url |
string | HTTPS URL of the main website. | |
from_name |
string | Sender name on emails, up to 120 characters. | |
brand_voice |
string | How the brand should sound, up to 2000 characters. Used by the AI writers. | |
font_family |
string | One of the typefaces KLCT offers. | |
socials |
object | Social profile URLs keyed by network (HTTPS only). |
Example
curl -X PUT "https://app.klctai.com/api/platform/branding" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"primary_color":"#1D4ED8","logo_url":"https://acme.example/logo.png"}'Response 200
{
"ok": true,
"updated": [
"primary_color",
"logo_url"
]
}Response fields
| Field | Type | Description |
|---|---|---|
ok |
boolean | True when saved. |
updated |
array | The fields that were changed. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "branding:write" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
Contacts
The people your account talks to. Contacts are shared by calls, chat, tickets, campaigns and sequences.
GET /contacts contacts:read List contacts
Newest first. Page through with page and per_page.
Operation id listContacts
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page |
integer | Page number, starting at 1. (default 1) | |
per_page |
integer | Rows per page, 1 to 100. (default 50) |
Example
curl "https://app.klctai.com/api/contacts" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"contacts": [
{
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": "+15551234567",
"company": "Acme Dental",
"website": null,
"country": "US",
"tags": [
"vip"
],
"metadata": {
"crm_id": "c_77"
},
"lead_status": "new",
"created_at": "2026-10-01T14:03:22Z"
}
],
"meta": {
"current_page": 1,
"last_page": 1,
"total": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
contacts |
array | The page of contacts. |
contacts[].contact_id |
integer | Contact id. |
contacts[].name |
string | First name. |
contacts[].last_name |
string | Last name. |
contacts[].email |
string | Email address. |
contacts[].phone |
string | Phone number in E.164 form (+15551234567). |
contacts[].company |
string | Company name. |
contacts[].website |
string | Website. |
contacts[].country |
string | Country. |
contacts[].tags |
array | Free-form tags. |
contacts[].metadata |
object | Your own key/value data. Merged on update, never replaced. |
contacts[].lead_status |
string | Pipeline stage KLCT assigned. |
contacts[].created_at |
string | When the contact was created. |
meta |
object | Pagination state. |
meta.current_page |
integer | Page just returned, starting at 1. |
meta.last_page |
integer | Last page available for this query. |
meta.total |
integer | Total rows matching the query. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "contacts:read" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
GET /contacts/{id} contacts:read Get a contact
Operation id getContact
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Contact id. |
Example
curl "https://app.klctai.com/api/contacts/123" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"contact": {
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": "+15551234567",
"company": "Acme Dental",
"website": null,
"country": "US",
"tags": [
"vip"
],
"metadata": {
"crm_id": "c_77"
},
"lead_status": "new",
"created_at": "2026-10-01T14:03:22Z",
"updated_at": "2026-10-02T09:00:00Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
contact |
object | |
contact.contact_id |
integer | Contact id. |
contact.name |
string | First name. |
contact.last_name |
string | Last name. |
contact.email |
string | Email address. |
contact.phone |
string | Phone number in E.164 form (+15551234567). |
contact.company |
string | Company name. |
contact.website |
string | Website. |
contact.country |
string | Country. |
contact.tags |
array | Free-form tags. |
contact.metadata |
object | Your own key/value data. Merged on update, never replaced. |
contact.lead_status |
string | Pipeline stage KLCT assigned. |
contact.created_at |
string | When the contact was created. |
contact.updated_at |
string | Last changed. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "contacts:read" scope (error: insufficient_scope).
- 404 No such record in this account.
- 429 Rate limit reached. Retry after the Retry-After header.
PATCH /contacts/{id} contacts:write Update a contact
Send only the fields to change. metadata is merged key by key, so you can add one key without resending the rest. Contacts are never deleted through the API.
Operation id updateContact
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Contact id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | First name. Cannot be blank. | |
last_name |
string | Last name. | |
email |
string | Email address. | |
phone |
string | Phone in any common format; stored as E.164. An unparseable value is rejected. | |
company |
string | Company. | |
website |
string | Website. | |
country |
string | Country. | |
metadata |
object | Merged into the existing metadata. |
Example
curl -X PATCH "https://app.klctai.com/api/contacts/123" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Dana","phone":"+15551234567"}'Response 200
{
"contact": {
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": "+15551234567",
"company": "Acme Dental",
"website": null,
"country": "US",
"tags": [
"vip"
],
"metadata": {
"crm_id": "c_77"
},
"lead_status": "new",
"created_at": "2026-10-01T14:03:22Z",
"updated_at": "2026-10-02T09:00:00Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
contact |
object | |
contact.contact_id |
integer | Contact id. |
contact.name |
string | First name. |
contact.last_name |
string | Last name. |
contact.email |
string | Email address. |
contact.phone |
string | Phone number in E.164 form (+15551234567). |
contact.company |
string | Company name. |
contact.website |
string | Website. |
contact.country |
string | Country. |
contact.tags |
array | Free-form tags. |
contact.metadata |
object | Your own key/value data. Merged on update, never replaced. |
contact.lead_status |
string | Pipeline stage KLCT assigned. |
contact.created_at |
string | When the contact was created. |
contact.updated_at |
string | Last changed. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "contacts:write" scope (error: insufficient_scope).
- 404 No such record in this account.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /contacts/create contacts:write Create one or many contacts
Send one contact's fields at the top level, or a contacts array to create several in one transaction (any invalid row rejects the whole batch). Phone must be +1 plus ten digits. Creating the same email twice creates two contacts; use POST /platform/contacts/sync when you want create-or-update by email.
Operation id createContacts
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | First name. |
last_name |
string | Yes | Last name. |
email |
string | Yes | Email address. |
phone |
string | Yes | +1 followed by ten digits. |
company |
string | Company. | |
website |
string | Website. | |
country |
string | Country. | |
metadata |
object | Your own key/value data. | |
contacts |
array | Alternative to the top-level fields: several contacts at once. |
Example
curl -X POST "https://app.klctai.com/api/contacts/create" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Dana","last_name":"Cole","email":"[email protected]","phone":"+15551234567","contacts":[]}'Response 200
{
"contacts": {
"contact_id": 90412,
"name": "Dana"
}
}Response fields
| Field | Type | Description |
|---|---|---|
contacts |
object | One object for a single contact, an array of them for a batch. |
contacts.contact_id |
integer | New contact id. |
contacts.name |
string | First name. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "contacts:write" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /platform/contacts/sync contacts:write Create or update contacts by email
Up to 200 contacts per call. A contact whose email already exists is updated: blank fields are filled, existing values are kept, tags are unioned and metadata is merged. Rows that fail validation are reported in errors without stopping the rest. Limited to 30 calls per minute.
Operation id syncContacts
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
contacts |
array | Yes | Between 1 and 200 contacts. |
contacts[].email |
string | Yes | Email, the match key. |
contacts[].name |
string | First name. | |
contacts[].last_name |
string | Last name. | |
contacts[].phone |
string | Any common phone format; ignored if it cannot be parsed. | |
contacts[].company |
string | Company. | |
contacts[].tags |
array | Up to 10 tags, 50 characters each. | |
contacts[].metadata |
object | Key/value data, merged. |
Example
curl -X POST "https://app.klctai.com/api/platform/contacts/sync" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contacts":[{"email":"[email protected]","name":"Dana","tags":[]}]}'Response 200
{
"created": 1,
"updated": 0,
"errors": []
}Response fields
| Field | Type | Description |
|---|---|---|
created |
integer | Contacts created. |
updated |
integer | Contacts updated. |
errors |
array | Rows that were skipped. |
errors[].index |
integer | Position in the request. |
errors[].error |
string | Why the row was skipped. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "contacts:write" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
Contact lists
Groups of contacts used as audiences for campaigns and AI call batches. These routes need a valid token but no specific scope.
GET /contactslists/list List contact lists
Operation id listContactLists
Example
curl "https://app.klctai.com/api/contactslists/list" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"data": [
{
"contacts_list_id": 12,
"name": "Spring leads",
"target_industry": "Dental"
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
data |
array | All lists in the account. |
Errors
- 401 Missing, invalid or expired token.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /contactslists/create Create a contact list
Operation id createContactList
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | List name. |
target_industry |
string | Yes | Industry the list targets. |
notes |
string | Yes | Notes about the list. |
metadata |
object | Your own key/value data. |
Example
curl -X POST "https://app.klctai.com/api/contactslists/create" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Spring leads","target_industry":"Dental","notes":"Imported from the open house"}'Response 200
{
"contact_list_id": 12
}Response fields
| Field | Type | Description |
|---|---|---|
contact_list_id |
integer | New list id. |
Errors
- 401 Missing, invalid or expired token.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /contactslists/{contactList}/update Rename or edit a contact list
Operation id updateContactList
Path parameters
| Name | Type | Description |
|---|---|---|
contactList | integer | Contact list id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | List name. |
target_industry |
string | Industry. | |
metadata |
object | Merged into existing metadata. |
Example
curl -X POST "https://app.klctai.com/api/contactslists/123/update" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Spring leads 2026"}'Response 200
{
"contact_list_id": 12
}Response fields
| Field | Type | Description |
|---|---|---|
contact_list_id |
integer | List id. |
Errors
- 401 Missing, invalid or expired token.
- 404 No such record in this account.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /contactslists/{contactList}/delete Delete a contact list
Deletes the list, not the contacts in it.
Operation id deleteContactList
Path parameters
| Name | Type | Description |
|---|---|---|
contactList | integer | Contact list id. |
Example
curl -X POST "https://app.klctai.com/api/contactslists/123/delete" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"success": "Contact list deleted with success!"
}Response fields
| Field | Type | Description |
|---|---|---|
success |
string | Confirmation sentence. |
Errors
- 401 Missing, invalid or expired token.
- 404 No such record in this account.
- 429 Rate limit reached. Retry after the Retry-After header.
GET /contactslists/{contactList}/get_contacts List the contacts in a list
Operation id listContactListMembers
Path parameters
| Name | Type | Description |
|---|---|---|
contactList | integer | Contact list id. |
Example
curl "https://app.klctai.com/api/contactslists/123/get_contacts" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"data": [
{
"contact_id": 90412,
"name": "Dana"
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
data |
array | Members of the list. |
Errors
- 401 Missing, invalid or expired token.
- 404 No such record in this account.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /contactslists/{contactList}/create_sync_contacts Create contacts and put them in a list
Creates each contact (or updates the existing one with the same phone or email) and adds it to the list.
Operation id createAndSyncListContacts
Path parameters
| Name | Type | Description |
|---|---|---|
contactList | integer | Contact list id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
contacts |
array | Yes | Contacts to create and add. |
contacts[].name |
string | Yes | First name. |
contacts[].email |
string | Yes | Email. |
contacts[].last_name |
string | Last name. | |
contacts[].phone |
string | Phone. | |
contacts[].company |
string | Company. | |
contacts[].website |
string | Website. | |
contacts[].country |
string | Country. | |
contacts[].metadata |
object | Key/value data. |
Example
curl -X POST "https://app.klctai.com/api/contactslists/123/create_sync_contacts" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contacts":[{"name":"Dana","email":"[email protected]","phone":"+15551234567"}]}'Response 200
{
"success": "The contacts where synced with success!"
}Response fields
| Field | Type | Description |
|---|---|---|
success |
string | Confirmation sentence. |
Errors
- 401 Missing, invalid or expired token.
- 404 No such record in this account.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /contactslists/{contactList}/sync_contacts Set the members of a list
Sets the list membership from contacts_ids.
Operation id syncListContacts
Path parameters
| Name | Type | Description |
|---|---|---|
contactList | integer | Contact list id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | List name (this endpoint requires it). |
contacts_ids |
array | Contact ids that should be in the list. |
Example
curl -X POST "https://app.klctai.com/api/contactslists/123/sync_contacts" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Spring leads","contacts_ids":[90412]}'Response 200
{
"success": "The contacts where synced with success!"
}Response fields
| Field | Type | Description |
|---|---|---|
success |
string | Confirmation sentence. |
Errors
- 401 Missing, invalid or expired token.
- 404 No such record in this account.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /contactslists/{contactList}/remove_contacts Remove contacts from a list
Removes membership only. The contacts themselves are kept.
Operation id removeListContacts
Path parameters
| Name | Type | Description |
|---|---|---|
contactList | integer | Contact list id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
contacts_ids |
array | Yes | Contact ids to remove. |
Example
curl -X POST "https://app.klctai.com/api/contactslists/123/remove_contacts" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contacts_ids":[90412]}'Response 200
{
"success": "Contacts removed"
}Response fields
| Field | Type | Description |
|---|---|---|
success |
string | Confirmation sentence. |
Errors
- 401 Missing, invalid or expired token.
- 404 No such record in this account.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
Calls
Calls the AI agents and your team made or received.
GET /calls calls:read List calls
Newest first, with outcome and duration.
Operation id listCalls
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page |
integer | Page number, starting at 1. (default 1) | |
per_page |
integer | Rows per page, 1 to 100. (default 50) |
Example
curl "https://app.klctai.com/api/calls" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"calls": [
{
"phone_call_id": 55120,
"contact_id": 90412,
"from": "+15559876543",
"to": "+15551234567",
"status": "completed",
"duration": 142,
"interest_level": "high",
"start_time": "2026-10-02T15:10:00Z",
"created_at": "2026-10-02T15:09:41Z"
}
],
"meta": {
"current_page": 1,
"last_page": 1,
"total": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
calls |
array | The page of calls. |
calls[].phone_call_id |
integer | Call id. |
calls[].contact_id |
integer | The contact on the call. |
calls[].from |
string | Caller number. |
calls[].to |
string | Dialed number. |
calls[].status |
string | Call status. |
calls[].duration |
integer | Seconds. |
calls[].interest_level |
string | How interested the AI graded the contact. |
calls[].start_time |
string | When the call started. |
calls[].created_at |
string | When the record was created. |
meta |
object | Pagination state. |
meta.current_page |
integer | Page just returned, starting at 1. |
meta.last_page |
integer | Last page available for this query. |
meta.total |
integer | Total rows matching the query. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "calls:read" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
Tickets
Helpdesk tickets from email, chat, forms and the API. Replies go to the customer by email, or into the chat when the ticket came from chat. Tickets are never deleted through the API. Needs the Support module on the account.
GET /tickets tickets:read List tickets
Newest first. Filter by status, contact, search text, or what changed since a time (handy for incremental sync).
Operation id listTickets
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page |
integer | Page number, starting at 1. (default 1) | |
per_page |
integer | Rows per page, 1 to 100. (default 50) | |
status |
string | One exact status, or `unresolved` for every status that still needs work. | |
q |
string | Matches the subject or ticket number. | |
contact_id |
integer | Only tickets for this contact. | |
updated_since |
string | Only tickets changed at or after this time. |
Example
curl "https://app.klctai.com/api/tickets?status=unresolved" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"tickets": [
{
"id": 1042,
"ticket_number": "SUP-1042",
"subject": "Cannot log in on mobile",
"status": "open",
"priority": 2,
"source": "email",
"contact": {
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": "+15551234567"
},
"due_at": null,
"first_response_at": null,
"resolved_at": null,
"created_at": "2026-10-14T09:12:00Z",
"updated_at": "2026-10-14T09:12:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 1,
"total": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
tickets |
array | The page of tickets. |
tickets[].id |
integer | Ticket id. |
tickets[].ticket_number |
string | Human reference such as SUP-1042. |
tickets[].subject |
string | Subject line. |
tickets[].status |
string | Current status. |
tickets[].priority |
integer | 0 (lowest) to 4 (highest). |
tickets[].source |
string | How it arrived: email, chat, form, api and others. |
tickets[].contact |
object | The requester, or null. |
tickets[].contact.contact_id |
integer | Contact id. |
tickets[].contact.name |
string | First name. |
tickets[].contact.last_name |
string | Last name. |
tickets[].contact.email |
string | Email. |
tickets[].contact.phone |
string | Phone. |
tickets[].due_at |
string | Due date, or null. |
tickets[].first_response_at |
string | When a person first replied, or null. |
tickets[].resolved_at |
string | When it was resolved, or null. |
tickets[].created_at |
string | Created. |
tickets[].updated_at |
string | Last changed. |
meta |
object | Pagination state. |
meta.current_page |
integer | Page just returned, starting at 1. |
meta.last_page |
integer | Last page available for this query. |
meta.total |
integer | Total rows matching the query. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "tickets:read" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
GET /tickets/{id} tickets:read Get a ticket with its conversation
Includes the customer-visible message thread. Internal notes and staff details beyond a display name are left out.
Operation id getTicket
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Ticket id. |
Example
curl "https://app.klctai.com/api/tickets/123" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"ticket": {
"id": 1042,
"ticket_number": "SUP-1042",
"subject": "Cannot log in on mobile",
"status": "open",
"priority": 2,
"source": "email",
"contact": {
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": "+15551234567"
},
"due_at": null,
"first_response_at": null,
"resolved_at": null,
"created_at": "2026-10-14T09:12:00Z",
"updated_at": "2026-10-14T09:12:00Z",
"description": "The app says my password is wrong but the website accepts it.",
"reply_channel": "email",
"conversation_id": null,
"messages": [
{
"id": 3301,
"direction": "in",
"sender_type": "contact",
"sender_name": "Dana Cole",
"body": "The app says my password is wrong but the website accepts it.",
"attachments": [],
"created_at": "2026-10-14T09:12:00Z"
}
]
}
}Response fields
| Field | Type | Description |
|---|---|---|
ticket |
object | |
ticket.id |
integer | Ticket id. |
ticket.ticket_number |
string | Human reference such as SUP-1042. |
ticket.subject |
string | Subject line. |
ticket.status |
string | Current status. |
ticket.priority |
integer | 0 (lowest) to 4 (highest). |
ticket.source |
string | How it arrived: email, chat, form, api and others. |
ticket.contact |
object | The requester, or null. |
ticket.contact.contact_id |
integer | Contact id. |
ticket.contact.name |
string | First name. |
ticket.contact.last_name |
string | Last name. |
ticket.contact.email |
string | Email. |
ticket.contact.phone |
string | Phone. |
ticket.due_at |
string | Due date, or null. |
ticket.first_response_at |
string | When a person first replied, or null. |
ticket.resolved_at |
string | When it was resolved, or null. |
ticket.created_at |
string | Created. |
ticket.updated_at |
string | Last changed. |
ticket.description |
string | The original request text. |
ticket.reply_channel |
string chat, email |
How a reply reaches the customer. |
ticket.conversation_id |
integer | The chat conversation behind the ticket, or null. |
ticket.messages |
array | The customer-visible thread. Internal notes are never included. |
ticket.messages[].id |
integer | Message id. |
ticket.messages[].direction |
string in, out |
in is from the customer, out is from your team. |
ticket.messages[].sender_type |
string | Who wrote it. |
ticket.messages[].sender_name |
string | Display name only. |
ticket.messages[].body |
string | Message text. |
ticket.messages[].attachments |
array | Attachment details. Files themselves are not downloadable through the API. |
ticket.messages[].attachments[].name |
string | File name. |
ticket.messages[].attachments[].mime |
string | Content type. |
ticket.messages[].attachments[].size |
integer | Bytes. |
ticket.messages[].created_at |
string | Sent. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "tickets:read" scope (error: insufficient_scope).
- 404 No such record in this account.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /tickets tickets:write Create a ticket
Creates an open ticket with source api. This records the ticket and fires the ticket.created webhook; it does not send an acknowledgement email to the requester.
Operation id createTicket
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
subject |
string | Yes | Subject line, up to 255 characters. |
description |
string | What happened, up to 20000 characters. | |
priority |
integer | 0 to 4. Defaults to 0. | |
contact_id |
integer | Requester. Must be a contact in this account. | |
due_at |
string | When it should be handled by. |
Example
curl -X POST "https://app.klctai.com/api/tickets" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject":"Cannot log in on mobile","description":"The app says my password is wrong.","priority":2,"contact_id":90412}'Response 201
{
"ticket": {
"id": 1042,
"ticket_number": "SUP-1042",
"subject": "Cannot log in on mobile",
"status": "open",
"priority": 2,
"source": "email",
"contact": {
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": "+15551234567"
},
"due_at": null,
"first_response_at": null,
"resolved_at": null,
"created_at": "2026-10-14T09:12:00Z",
"updated_at": "2026-10-14T09:12:00Z",
"description": "The app says my password is wrong but the website accepts it.",
"reply_channel": "email",
"conversation_id": null,
"messages": [
{
"id": 3301,
"direction": "in",
"sender_type": "contact",
"sender_name": "Dana Cole",
"body": "The app says my password is wrong but the website accepts it.",
"attachments": [],
"created_at": "2026-10-14T09:12:00Z"
}
]
}
}Response fields
| Field | Type | Description |
|---|---|---|
ticket |
object | |
ticket.id |
integer | Ticket id. |
ticket.ticket_number |
string | Human reference such as SUP-1042. |
ticket.subject |
string | Subject line. |
ticket.status |
string | Current status. |
ticket.priority |
integer | 0 (lowest) to 4 (highest). |
ticket.source |
string | How it arrived: email, chat, form, api and others. |
ticket.contact |
object | The requester, or null. |
ticket.contact.contact_id |
integer | Contact id. |
ticket.contact.name |
string | First name. |
ticket.contact.last_name |
string | Last name. |
ticket.contact.email |
string | Email. |
ticket.contact.phone |
string | Phone. |
ticket.due_at |
string | Due date, or null. |
ticket.first_response_at |
string | When a person first replied, or null. |
ticket.resolved_at |
string | When it was resolved, or null. |
ticket.created_at |
string | Created. |
ticket.updated_at |
string | Last changed. |
ticket.description |
string | The original request text. |
ticket.reply_channel |
string chat, email |
How a reply reaches the customer. |
ticket.conversation_id |
integer | The chat conversation behind the ticket, or null. |
ticket.messages |
array | The customer-visible thread. Internal notes are never included. |
ticket.messages[].id |
integer | Message id. |
ticket.messages[].direction |
string in, out |
in is from the customer, out is from your team. |
ticket.messages[].sender_type |
string | Who wrote it. |
ticket.messages[].sender_name |
string | Display name only. |
ticket.messages[].body |
string | Message text. |
ticket.messages[].attachments |
array | Attachment details. Files themselves are not downloadable through the API. |
ticket.messages[].attachments[].name |
string | File name. |
ticket.messages[].attachments[].mime |
string | Content type. |
ticket.messages[].attachments[].size |
integer | Bytes. |
ticket.messages[].created_at |
string | Sent. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "tickets:write" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
PATCH /tickets/{id} tickets:write Update a ticket
Send only the fields to change. Moving to resolved or closed stamps resolved_at; moving a resolved ticket back to an open status counts as a reopen. Each change is written to the ticket history.
Operation id updateTicket
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Ticket id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
subject |
string | New subject. | |
description |
string | New description. | |
priority |
integer | 0 to 4. | |
status |
string | A valid ticket status, for example open, pending, resolved, closed. | |
due_at |
string | New due date. |
Example
curl -X PATCH "https://app.klctai.com/api/tickets/123" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"priority":1,"status":"resolved"}'Response 200
{
"ticket": {
"status": "resolved",
"id": 1042,
"ticket_number": "SUP-1042",
"subject": "Cannot log in on mobile",
"priority": 2,
"source": "email",
"contact": {
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": "+15551234567"
},
"due_at": null,
"first_response_at": null,
"resolved_at": null,
"created_at": "2026-10-14T09:12:00Z",
"updated_at": "2026-10-14T09:12:00Z",
"description": "The app says my password is wrong but the website accepts it.",
"reply_channel": "email",
"conversation_id": null,
"messages": [
{
"id": 3301,
"direction": "in",
"sender_type": "contact",
"sender_name": "Dana Cole",
"body": "The app says my password is wrong but the website accepts it.",
"attachments": [],
"created_at": "2026-10-14T09:12:00Z"
}
]
}
}Response fields
| Field | Type | Description |
|---|---|---|
ticket |
object | |
ticket.id |
integer | Ticket id. |
ticket.ticket_number |
string | Human reference such as SUP-1042. |
ticket.subject |
string | Subject line. |
ticket.status |
string | Current status. |
ticket.priority |
integer | 0 (lowest) to 4 (highest). |
ticket.source |
string | How it arrived: email, chat, form, api and others. |
ticket.contact |
object | The requester, or null. |
ticket.contact.contact_id |
integer | Contact id. |
ticket.contact.name |
string | First name. |
ticket.contact.last_name |
string | Last name. |
ticket.contact.email |
string | Email. |
ticket.contact.phone |
string | Phone. |
ticket.due_at |
string | Due date, or null. |
ticket.first_response_at |
string | When a person first replied, or null. |
ticket.resolved_at |
string | When it was resolved, or null. |
ticket.created_at |
string | Created. |
ticket.updated_at |
string | Last changed. |
ticket.description |
string | The original request text. |
ticket.reply_channel |
string chat, email |
How a reply reaches the customer. |
ticket.conversation_id |
integer | The chat conversation behind the ticket, or null. |
ticket.messages |
array | The customer-visible thread. Internal notes are never included. |
ticket.messages[].id |
integer | Message id. |
ticket.messages[].direction |
string in, out |
in is from the customer, out is from your team. |
ticket.messages[].sender_type |
string | Who wrote it. |
ticket.messages[].sender_name |
string | Display name only. |
ticket.messages[].body |
string | Message text. |
ticket.messages[].attachments |
array | Attachment details. Files themselves are not downloadable through the API. |
ticket.messages[].attachments[].name |
string | File name. |
ticket.messages[].attachments[].mime |
string | Content type. |
ticket.messages[].attachments[].size |
integer | Bytes. |
ticket.messages[].created_at |
string | Sent. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "tickets:write" scope (error: insufficient_scope).
- 404 No such record in this account.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /tickets/{id}/notes tickets:write Add an internal note
A note for your team. It is recorded in the ticket history and is never shown to or sent to the customer.
Operation id addTicketNote
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Ticket id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
body |
string | Yes | Note text, up to 10000 characters. |
Example
curl -X POST "https://app.klctai.com/api/tickets/123/notes" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"Reproduced on iOS 19. Passing to engineering."}'Response 201
{
"note": {
"id": 5521,
"body": "Reproduced on iOS 19. Passing to engineering.",
"created_at": "2026-10-14T10:02:00Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
note |
object | |
note.id |
integer | Note id. |
note.body |
string | Note text. |
note.created_at |
string | Created. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "tickets:write" scope (error: insufficient_scope).
- 404 No such record in this account.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /tickets/{id}/replies tickets:write Reply to the customer
Sends a real, customer-visible reply: by email, or into the chat if the ticket came from chat (which also takes the chat over from the AI). Records the first response time. Answers 422 if the requester has no email address or the send fails, and nothing is stored in that case.
Operation id replyToTicket
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Ticket id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
body |
string | Yes | Reply text, up to 16000 characters. |
Example
curl -X POST "https://app.klctai.com/api/tickets/123/replies" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"Thanks Dana, we found the cause and a fix is rolling out today."}'Response 201
{
"message": {
"id": 3302,
"channel": "email",
"direction": "out",
"body": "Thanks Dana, we found the cause and a fix is rolling out today.",
"created_at": "2026-10-14T10:05:00Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
message |
object | |
message.id |
integer | Message id. |
message.channel |
string chat, email |
How it was sent. |
message.direction |
string | Always out. |
message.body |
string | Text sent. |
message.created_at |
string | Sent. |
Errors
- 401 Missing, invalid or expired token.
- 403 Insufficient scope, or the Support module is switched off for the account.
- 404 No such record in this account.
- 422 Validation failed, or the reply could not be delivered (for example the requester has no email).
- 429 Rate limit reached. Retry after the Retry-After header.
Chat conversations
Live chat and messaging threads (web chat, WhatsApp, Instagram, email). Needs the Support module on the account.
GET /conversations conversations:read List chat conversations
Most recently active first.
Operation id listConversations
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page |
integer | Page number, starting at 1. (default 1) | |
per_page |
integer | Rows per page, 1 to 100. (default 50) | |
status |
string | One exact status, or `unresolved` for everything not resolved or closed. | |
contact_id |
integer | Only conversations with this contact. | |
updated_since |
string | Only conversations changed at or after this time. |
Example
curl "https://app.klctai.com/api/conversations?status=unresolved" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"conversations": [
{
"id": 7781,
"status": "open",
"handling_mode": "ai",
"channel": "web",
"subject": null,
"priority": "normal",
"contact": {
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": null
},
"ticket_id": null,
"last_message_at": "2026-10-14T09:20:00Z",
"resolved_at": null,
"csat_rating": null,
"created_at": "2026-10-14T09:18:00Z",
"updated_at": "2026-10-14T09:20:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 1,
"total": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
conversations |
array | The page of conversations. |
conversations[].id |
integer | Conversation id. |
conversations[].status |
string | open, pending, resolved, closed and others. |
conversations[].handling_mode |
string ai, human |
Who is answering: the AI assistant or a person. |
conversations[].channel |
string | web, email, whatsapp, instagram and others. |
conversations[].subject |
string | Short title, if any. |
conversations[].priority |
string | Priority label. |
conversations[].contact |
object | The person chatting, or null if anonymous. |
conversations[].contact.contact_id |
integer | Contact id. |
conversations[].contact.name |
string | First name. |
conversations[].contact.last_name |
string | Last name. |
conversations[].contact.email |
string | Email. |
conversations[].contact.phone |
string | Phone. |
conversations[].ticket_id |
integer | Linked ticket, or null. |
conversations[].last_message_at |
string | Most recent customer-visible message. |
conversations[].resolved_at |
string | When it was resolved, or null. |
conversations[].csat_rating |
integer | The customer satisfaction rating, or null. |
conversations[].created_at |
string | Started. |
conversations[].updated_at |
string | Last changed. |
meta |
object | Pagination state. |
meta.current_page |
integer | Page just returned, starting at 1. |
meta.last_page |
integer | Last page available for this query. |
meta.total |
integer | Total rows matching the query. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "conversations:read" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
GET /conversations/{id} conversations:read Get a conversation with its messages
Reading a conversation does not mark it read for your team.
Operation id getConversation
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Conversation id. |
Example
curl "https://app.klctai.com/api/conversations/123" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"conversation": {
"id": 7781,
"status": "open",
"handling_mode": "ai",
"channel": "web",
"subject": null,
"priority": "normal",
"contact": {
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": null
},
"ticket_id": null,
"last_message_at": "2026-10-14T09:20:00Z",
"resolved_at": null,
"csat_rating": null,
"created_at": "2026-10-14T09:18:00Z",
"updated_at": "2026-10-14T09:20:00Z",
"messages": [
{
"id": 1,
"sender_type": "visitor",
"sender_name": "Dana Cole",
"body": "Do you open on Sundays?",
"attachments": [],
"created_at": "2026-10-14T09:18:00Z"
},
{
"id": 2,
"sender_type": "ai",
"sender_name": "Assistant",
"body": "Yes, 10am to 4pm.",
"attachments": [],
"created_at": "2026-10-14T09:18:05Z"
}
]
}
}Response fields
| Field | Type | Description |
|---|---|---|
conversation |
object | |
conversation.id |
integer | Conversation id. |
conversation.status |
string | open, pending, resolved, closed and others. |
conversation.handling_mode |
string ai, human |
Who is answering: the AI assistant or a person. |
conversation.channel |
string | web, email, whatsapp, instagram and others. |
conversation.subject |
string | Short title, if any. |
conversation.priority |
string | Priority label. |
conversation.contact |
object | The person chatting, or null if anonymous. |
conversation.contact.contact_id |
integer | Contact id. |
conversation.contact.name |
string | First name. |
conversation.contact.last_name |
string | Last name. |
conversation.contact.email |
string | Email. |
conversation.contact.phone |
string | Phone. |
conversation.ticket_id |
integer | Linked ticket, or null. |
conversation.last_message_at |
string | Most recent customer-visible message. |
conversation.resolved_at |
string | When it was resolved, or null. |
conversation.csat_rating |
integer | The customer satisfaction rating, or null. |
conversation.created_at |
string | Started. |
conversation.updated_at |
string | Last changed. |
conversation.messages |
array | Customer-visible messages, oldest first. Internal notes are never included. |
conversation.messages[].id |
integer | Message id. |
conversation.messages[].sender_type |
string visitor, operator, ai |
Who wrote it. |
conversation.messages[].sender_name |
string | Display name only. |
conversation.messages[].body |
string | Message text. |
conversation.messages[].attachments |
array | Attachment details only. |
conversation.messages[].attachments[].name |
string | File name. |
conversation.messages[].attachments[].mime |
string | Content type. |
conversation.messages[].attachments[].size |
integer | Bytes. |
conversation.messages[].created_at |
string | Sent. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "conversations:read" scope (error: insufficient_scope).
- 404 No such record in this account.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /conversations/{id}/messages conversations:write Send a reply in a conversation
Sends a real message to the customer on the conversation's channel, and takes the conversation over from the AI (handling_mode becomes human).
Operation id replyToConversation
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Conversation id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
body |
string | Yes | Message text, up to 16000 characters. |
Example
curl -X POST "https://app.klctai.com/api/conversations/123/messages" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"Yes, we are open 10am to 4pm on Sundays."}'Response 201
{
"message": {
"id": 3,
"sender_type": "operator",
"body": "Yes, we are open 10am to 4pm on Sundays.",
"created_at": "2026-10-14T09:25:00Z"
},
"handling_mode": "human"
}Response fields
| Field | Type | Description |
|---|---|---|
message |
object | |
message.id |
integer | Message id. |
message.sender_type |
string | Always operator. |
message.body |
string | Text sent. |
message.created_at |
string | Sent. |
handling_mode |
string | Now human. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "conversations:write" scope (error: insufficient_scope).
- 404 No such record in this account.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /conversations/{id}/resolve conversations:write Resolve a conversation
Marks the conversation resolved. The customer may be asked for a rating, depending on the widget settings.
Operation id resolveConversation
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Conversation id. |
Example
curl -X POST "https://app.klctai.com/api/conversations/123/resolve" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"conversation": {
"status": "resolved",
"id": 7781,
"handling_mode": "ai",
"channel": "web",
"subject": null,
"priority": "normal",
"contact": {
"contact_id": 90412,
"name": "Dana",
"last_name": "Cole",
"email": "[email protected]",
"phone": null
},
"ticket_id": null,
"last_message_at": "2026-10-14T09:20:00Z",
"resolved_at": null,
"csat_rating": null,
"created_at": "2026-10-14T09:18:00Z",
"updated_at": "2026-10-14T09:20:00Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
conversation |
object | |
conversation.id |
integer | Conversation id. |
conversation.status |
string | open, pending, resolved, closed and others. |
conversation.handling_mode |
string ai, human |
Who is answering: the AI assistant or a person. |
conversation.channel |
string | web, email, whatsapp, instagram and others. |
conversation.subject |
string | Short title, if any. |
conversation.priority |
string | Priority label. |
conversation.contact |
object | The person chatting, or null if anonymous. |
conversation.contact.contact_id |
integer | Contact id. |
conversation.contact.name |
string | First name. |
conversation.contact.last_name |
string | Last name. |
conversation.contact.email |
string | Email. |
conversation.contact.phone |
string | Phone. |
conversation.ticket_id |
integer | Linked ticket, or null. |
conversation.last_message_at |
string | Most recent customer-visible message. |
conversation.resolved_at |
string | When it was resolved, or null. |
conversation.csat_rating |
integer | The customer satisfaction rating, or null. |
conversation.created_at |
string | Started. |
conversation.updated_at |
string | Last changed. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "conversations:write" scope (error: insufficient_scope).
- 404 No such record in this account.
- 429 Rate limit reached. Retry after the Retry-After header.
Changelog
The release notes on your customer portal. Use this to publish from a release pipeline. Publishing through the API never emails subscribers; entries are never deleted. Needs the Changelog module on the account.
GET /changelog changelog:read List changelog entries
Operation id listChangelog
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page |
integer | Page number, starting at 1. (default 1) | |
per_page |
integer | Rows per page, 1 to 100. (default 50) | |
status |
string draft, published |
Only drafts or only published entries. |
Example
curl "https://app.klctai.com/api/changelog" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"entries": [
{
"id": 214,
"title": "Faster ticket search",
"slug": "faster-ticket-search",
"version": "v2.4.0",
"category": {
"id": 3,
"name": "Improvements"
},
"labels": [
"New"
],
"status": "published",
"is_live": true,
"published_at": "2026-10-14T12:00:00Z",
"created_at": "2026-10-14T11:40:00Z",
"updated_at": "2026-10-14T11:40:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 1,
"total": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
entries |
array | The page of entries. |
entries[].id |
integer | Entry id. |
entries[].title |
string | Title. |
entries[].slug |
string | URL slug, unique in the portal. |
entries[].version |
string | Version label such as v2.4.0, or null. |
entries[].category |
object | Category, or null. |
entries[].category.id |
integer | Category id. |
entries[].category.name |
string | Category name. |
entries[].labels |
array | Free-form labels such as New or Fix. |
entries[].status |
string draft, published |
draft is private, published is public once its date arrives. |
entries[].is_live |
boolean | True when published and its date has passed, so visitors can see it. |
entries[].published_at |
string | Publish time, or null for a draft. |
entries[].created_at |
string | Created. |
entries[].updated_at |
string | Last changed. |
meta |
object | Pagination state. |
meta.current_page |
integer | Page just returned, starting at 1. |
meta.last_page |
integer | Last page available for this query. |
meta.total |
integer | Total rows matching the query. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "changelog:read" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
GET /changelog/{id} changelog:read Get a changelog entry
Operation id getChangelogEntry
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Entry id. |
Example
curl "https://app.klctai.com/api/changelog/123" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"entry": {
"id": 214,
"title": "Faster ticket search",
"slug": "faster-ticket-search",
"version": "v2.4.0",
"category": {
"id": 3,
"name": "Improvements"
},
"labels": [
"New"
],
"status": "published",
"is_live": true,
"published_at": "2026-10-14T12:00:00Z",
"created_at": "2026-10-14T11:40:00Z",
"updated_at": "2026-10-14T11:40:00Z",
"content_html": "<p>Searching tickets is now twice as fast.</p>"
}
}Response fields
| Field | Type | Description |
|---|---|---|
entry |
object | |
entry.id |
integer | Entry id. |
entry.title |
string | Title. |
entry.slug |
string | URL slug, unique in the portal. |
entry.version |
string | Version label such as v2.4.0, or null. |
entry.category |
object | Category, or null. |
entry.category.id |
integer | Category id. |
entry.category.name |
string | Category name. |
entry.labels |
array | Free-form labels such as New or Fix. |
entry.status |
string draft, published |
draft is private, published is public once its date arrives. |
entry.is_live |
boolean | True when published and its date has passed, so visitors can see it. |
entry.published_at |
string | Publish time, or null for a draft. |
entry.created_at |
string | Created. |
entry.updated_at |
string | Last changed. |
entry.content_html |
string | The body as sanitized HTML. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "changelog:read" scope (error: insufficient_scope).
- 404 No such record in this account.
- 429 Rate limit reached. Retry after the Retry-After header.
POST /changelog changelog:write Create a changelog entry
Saved as a draft unless you send status: published. The slug is generated from the title. Publishing fires the changelog.published webhook. Subscribers are not emailed.
Operation id createChangelogEntry
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Title, 3 to 255 characters. |
content |
string | Yes | Body up to 60000 characters. Plain text, Markdown or HTML; HTML is sanitized. |
version |
string | Version label, up to 50 characters. | |
category_id |
integer | One of this portal's categories. | |
labels |
array | Up to 10 labels, 40 characters each. | |
status |
string draft, published |
Defaults to draft on create. | |
published_at |
string | When it goes live. Defaults to now when publishing. |
Example
curl -X POST "https://app.klctai.com/api/changelog" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Faster ticket search","content":"Searching tickets is now **twice as fast**.","version":"v2.4.0","category_id":1,"labels":["New"],"status":"draft"}'Response 201
{
"entry": {
"id": 214,
"title": "Faster ticket search",
"slug": "faster-ticket-search",
"version": "v2.4.0",
"category": {
"id": 3,
"name": "Improvements"
},
"labels": [
"New"
],
"status": "published",
"is_live": true,
"published_at": "2026-10-14T12:00:00Z",
"created_at": "2026-10-14T11:40:00Z",
"updated_at": "2026-10-14T11:40:00Z",
"content_html": "<p>Searching tickets is now twice as fast.</p>"
}
}Response fields
| Field | Type | Description |
|---|---|---|
entry |
object | |
entry.id |
integer | Entry id. |
entry.title |
string | Title. |
entry.slug |
string | URL slug, unique in the portal. |
entry.version |
string | Version label such as v2.4.0, or null. |
entry.category |
object | Category, or null. |
entry.category.id |
integer | Category id. |
entry.category.name |
string | Category name. |
entry.labels |
array | Free-form labels such as New or Fix. |
entry.status |
string draft, published |
draft is private, published is public once its date arrives. |
entry.is_live |
boolean | True when published and its date has passed, so visitors can see it. |
entry.published_at |
string | Publish time, or null for a draft. |
entry.created_at |
string | Created. |
entry.updated_at |
string | Last changed. |
entry.content_html |
string | The body as sanitized HTML. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "changelog:write" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
PATCH /changelog/{id} changelog:write Update a changelog entry
Send only the fields to change; an empty body is rejected. Published entries can be edited. Moving a draft to published fires changelog.published once.
Operation id updateChangelogEntry
Path parameters
| Name | Type | Description |
|---|---|---|
id | integer | Entry id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Title, 3 to 255 characters. | |
content |
string | Body up to 60000 characters. Plain text, Markdown or HTML; HTML is sanitized. | |
version |
string | Version label, up to 50 characters. | |
category_id |
integer | One of this portal's categories. | |
labels |
array | Up to 10 labels, 40 characters each. | |
status |
string draft, published |
Defaults to draft on create. | |
published_at |
string | When it goes live. Defaults to now when publishing. |
Example
curl -X PATCH "https://app.klctai.com/api/changelog/123" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Faster ticket search","content":"Searching tickets is now **twice as fast**.","version":"v2.4.0","category_id":1,"labels":["New"],"status":"draft"}'Response 200
{
"entry": {
"id": 214,
"title": "Faster ticket search",
"slug": "faster-ticket-search",
"version": "v2.4.0",
"category": {
"id": 3,
"name": "Improvements"
},
"labels": [
"New"
],
"status": "published",
"is_live": true,
"published_at": "2026-10-14T12:00:00Z",
"created_at": "2026-10-14T11:40:00Z",
"updated_at": "2026-10-14T11:40:00Z",
"content_html": "<p>Searching tickets is now twice as fast.</p>"
}
}Response fields
| Field | Type | Description |
|---|---|---|
entry |
object | |
entry.id |
integer | Entry id. |
entry.title |
string | Title. |
entry.slug |
string | URL slug, unique in the portal. |
entry.version |
string | Version label such as v2.4.0, or null. |
entry.category |
object | Category, or null. |
entry.category.id |
integer | Category id. |
entry.category.name |
string | Category name. |
entry.labels |
array | Free-form labels such as New or Fix. |
entry.status |
string draft, published |
draft is private, published is public once its date arrives. |
entry.is_live |
boolean | True when published and its date has passed, so visitors can see it. |
entry.published_at |
string | Publish time, or null for a draft. |
entry.created_at |
string | Created. |
entry.updated_at |
string | Last changed. |
entry.content_html |
string | The body as sanitized HTML. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "changelog:write" scope (error: insufficient_scope).
- 404 No such record in this account.
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
Feedback and roadmap
The feature requests and roadmap from your customer portal. Read-only. Author names and emails are never returned. Needs the Feedback board and Roadmap modules on the account.
GET /feedback feedback:read List feedback posts
Most voted first.
Operation id listFeedback
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page |
integer | Page number, starting at 1. (default 1) | |
per_page |
integer | Rows per page, 1 to 100. (default 50) | |
status |
string | Board status slug. | |
category_id |
integer | Only posts in this category. | |
q |
string | Matches the title or text. |
Example
curl "https://app.klctai.com/api/feedback?status=planned" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"posts": [
{
"id": 88,
"title": "Dark mode",
"content": "Please add a dark theme to the dashboard.",
"status": "planned",
"category": {
"id": 2,
"name": "Feature request"
},
"vote_count": 41,
"comments_count": 6,
"on_roadmap": true,
"created_at": "2026-09-02T08:30:00Z",
"updated_at": "2026-10-01T10:00:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 1,
"total": 1
}
}Response fields
| Field | Type | Description |
|---|---|---|
posts |
array | The page of posts. |
posts[].id |
integer | Post id. |
posts[].title |
string | Title. |
posts[].content |
string | What the customer wrote. |
posts[].status |
string | Board status slug. |
posts[].category |
object | Category, or null. |
posts[].category.id |
integer | Category id. |
posts[].category.name |
string | Name. |
posts[].vote_count |
integer | Votes. |
posts[].comments_count |
integer | Comments. |
posts[].on_roadmap |
boolean | Whether the post sits on a roadmap column. |
posts[].created_at |
string | Posted. |
posts[].updated_at |
string | Last changed. |
meta |
object | Pagination state. |
meta.current_page |
integer | Page just returned, starting at 1. |
meta.last_page |
integer | Last page available for this query. |
meta.total |
integer | Total rows matching the query. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "feedback:read" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
GET /roadmap feedback:read Get the roadmap
The roadmap columns in display order, each with its posts. Not paginated.
Operation id getRoadmap
Example
curl "https://app.klctai.com/api/roadmap" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"statuses": [
{
"slug": "planned",
"name": "Planned",
"color": "#6366f1",
"posts": [
{
"id": 88,
"title": "Dark mode",
"content": "Please add a dark theme to the dashboard.",
"status": "planned",
"category": {
"id": 2,
"name": "Feature request"
},
"vote_count": 41,
"comments_count": 6,
"on_roadmap": true,
"created_at": "2026-09-02T08:30:00Z",
"updated_at": "2026-10-01T10:00:00Z"
}
]
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
statuses |
array | Roadmap columns. |
statuses[].slug |
string | Column slug. |
statuses[].name |
string | Column name. |
statuses[].color |
string | Column color. |
statuses[].posts |
array | Posts in this column. |
statuses[].posts[].id |
integer | Post id. |
statuses[].posts[].title |
string | Title. |
statuses[].posts[].content |
string | What the customer wrote. |
statuses[].posts[].status |
string | Board status slug. |
statuses[].posts[].category |
object | Category, or null. |
statuses[].posts[].category.id |
integer | Category id. |
statuses[].posts[].category.name |
string | Name. |
statuses[].posts[].vote_count |
integer | Votes. |
statuses[].posts[].comments_count |
integer | Comments. |
statuses[].posts[].on_roadmap |
boolean | Whether the post sits on a roadmap column. |
statuses[].posts[].created_at |
string | Posted. |
statuses[].posts[].updated_at |
string | Last changed. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "feedback:read" scope (error: insufficient_scope).
- 429 Rate limit reached. Retry after the Retry-After header.
Events and webhooks
Send events into KLCT to start sequences, and subscribe to events KLCT sends back. See the Webhooks guide for payloads and the signature check.
POST /integrations/sprwt/events sequences:trigger Forward a product event to start a sequence
Forwards something that happened in your product (order placed, cart abandoned, subscription renewed) so a matching KLCT sequence can enroll the contact. Contacts created this way are quarantined "shadow" contacts unless the account has opted in to exporting real contacts. If attribution carries ad parameters and trigger is order_create, user_create or user_subscriptions_create, a conversion is recorded for ad optimisation.
Operation id sendEvent
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
trigger |
string | Yes | Event key a sequence trigger listens for. |
external_ref |
string | Your own id for the event, for traceability. | |
contact |
object | Yes | Who the event is about. |
contact.name |
string | Yes | First name. |
contact.last_name |
string | Last name. | |
contact.email |
string | Email. Either email or phone is required. | |
contact.phone |
string | Phone. | |
metadata |
object | Extra data stored on the contact. | |
attribution |
object | utm_source, utm_medium, utm_campaign, utm_content, utm_term, fbclid, gclid, klct_ad. | |
kpi |
object | Revenue for purchase events. | |
kpi.value_minor |
integer | Order value in minor units (cents). | |
kpi.currency |
string | ISO currency code. |
Example
curl -X POST "https://app.klctai.com/api/integrations/sprwt/events" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"trigger":"order_create","external_ref":"order_8841","contact":{"name":"Dana","email":"[email protected]"},"kpi":{"value_minor":4900,"currency":"USD"}}'Response 200
{
"status": "ok",
"enrolled": 1
}Response fields
| Field | Type | Description |
|---|---|---|
status |
string | "ok". |
enrolled |
integer | How many sequences the contact was enrolled in. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "sequences:trigger" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
GET /oauth-platform/webhooks webhooks:manage List your webhook subscriptions
Only subscriptions created by the calling connection are visible. Requires an OAuth connection, not a manual API key.
Operation id listWebhooks
Example
curl "https://app.klctai.com/api/oauth-platform/webhooks" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"webhooks": [
{
"id": 7,
"event_type": "contact.created",
"target_url": "https://acme.example/hooks/klct",
"is_active": true,
"created_at": "2026-10-01T12:00:00Z"
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
webhooks |
array | Subscriptions. |
webhooks[].id |
integer | Subscription id. |
webhooks[].event_type |
string | Event name. |
webhooks[].target_url |
string | Your HTTPS endpoint. |
webhooks[].is_active |
boolean | Whether deliveries are on. |
webhooks[].created_at |
string | Created. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "webhooks:manage" scope (error: insufficient_scope).
- 429 Rate limit reached. Retry after the Retry-After header.
POST /oauth-platform/webhooks webhooks:manage Subscribe to an event
The signing_secret is returned once. Store it; you need it to verify the X-KLCT-Signature header. Requires an OAuth connection, not a manual API key.
Operation id createWebhook
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
event_type |
string | Yes | One of the supported events (see the Webhooks guide). |
target_url |
string | Yes | HTTPS URL KLCT will POST to. |
Example
curl -X POST "https://app.klctai.com/api/oauth-platform/webhooks" \
-H "Authorization: Bearer $KLCT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event_type":"contact.created","target_url":"https://acme.example/hooks/klct"}'Response 201
{
"id": 7,
"event_type": "contact.created",
"target_url": "https://acme.example/hooks/klct",
"signing_secret": "3f8c0b6d1e..."
}Response fields
| Field | Type | Description |
|---|---|---|
id |
integer | Subscription id. |
event_type |
string | Event name. |
target_url |
string | Your URL. |
signing_secret |
string | HMAC secret. Shown once. |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "webhooks:manage" scope (error: insufficient_scope).
- 422 Validation failed.
- 429 Rate limit reached. Retry after the Retry-After header.
DELETE /oauth-platform/webhooks/{webhook} webhooks:manage Remove a webhook subscription
Operation id deleteWebhook
Path parameters
| Name | Type | Description |
|---|---|---|
webhook | integer | Subscription id. |
Example
curl -X DELETE "https://app.klctai.com/api/oauth-platform/webhooks/123" \
-H "Authorization: Bearer $KLCT_API_KEY"Response 200
{
"status": "ok"
}Response fields
| Field | Type | Description |
|---|---|---|
status |
string | "ok". |
Errors
- 401 Missing, invalid or expired token.
- 403 The token does not carry the "webhooks:manage" scope (error: insufficient_scope).
- 404 No such record in this account.
- 429 Rate limit reached. Retry after the Retry-After header.
Meetings
Bookable meeting types, for embedding a booking link in another product.
GET /{account}/meetings first-party token List an account's bookable meetings
First-party only. Uses the shared machine-to-machine token (APP_API_TOKEN) instead of an account key, and is keyed by account id because slugs can be renamed. Account API keys and OAuth tokens are not accepted here.
Operation id listMeetings
Path parameters
| Name | Type | Description |
|---|---|---|
account | integer | KLCT account id. |
Example
curl "https://app.klctai.com/api/123/meetings" \
-H "Authorization: Bearer $KLCT_APP_API_TOKEN"Response 200
{
"ok": true,
"account": {
"id": 4821,
"name": "Acme Dental"
},
"meetings": [
{
"id": 31,
"name": "Intro call",
"slug": "intro-call",
"description": "15 minutes to talk through your goals.",
"duration_minutes": 15,
"location_type": "video",
"url": "https://app.klctai.com/book/acme/intro-call"
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
ok |
boolean | True. |
account |
object | |
account.id |
integer | Account id. |
account.name |
string | Account name. |
meetings |
array | |
meetings[].id |
integer | Meeting type id. |
meetings[].name |
string | Name. |
meetings[].slug |
string | URL slug. |
meetings[].description |
string | Description. |
meetings[].duration_minutes |
integer | Length. |
meetings[].location_type |
string | Where it happens (video, phone, in person). |
meetings[].url |
string | Public booking URL. |
Errors
- 401 Missing, invalid or expired token.
- 404 No such record in this account.
- 429 Rate limit reached. Retry after the Retry-After header.