# API reference

Base URL: `https://app.klctai.com/api`

## Account

Identify the account a token belongs to and keep its brand in step with your product.

### Get the connected account

`GET /platform/account`

Operation id: `getAccount`. Scope: `identity`.

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.

```bash
curl "https://app.klctai.com/api/platform/account" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "account_id": 4821,
    "name": "Acme Dental",
    "billing_active": true,
    "setup_url": null
}
```

### Set the account brand

`PUT /platform/branding`

Operation id: `updateBranding`. Scope: `branding:write`.

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.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `primary_color` | string | no | Hex color, for example #1D4ED8. |
| `secondary_color` | string | no | Hex color. |
| `accent_color` | string | no | Hex color. |
| `button_color` | string | no | Hex color for buttons. |
| `button_text_color` | string | no | Hex color for button text. |
| `logo_url` | string | no | HTTPS URL of the logo. |
| `email_logo_url` | string | no | HTTPS URL of the logo used in email headers. |
| `square_logo_url` | string | no | HTTPS URL of a square logo. |
| `favicon_url` | string | no | HTTPS URL of the favicon. |
| `website_url` | string | no | HTTPS URL of the main website. |
| `from_name` | string | no | Sender name on emails, up to 120 characters. |
| `brand_voice` | string | no | How the brand should sound, up to 2000 characters. Used by the AI writers. |
| `font_family` | string | no | One of the typefaces KLCT offers. |
| `socials` | object | no | Social profile URLs keyed by network (HTTPS only). |

```bash
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`:

```json
{
    "ok": true,
    "updated": [
        "primary_color",
        "logo_url"
    ]
}
```

## Contacts

The people your account talks to. Contacts are shared by calls, chat, tickets, campaigns and sequences.

### List contacts

`GET /contacts`

Operation id: `listContacts`. Scope: `contacts:read`.

Newest first. Page through with `page` and `per_page`.

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer | no | Page number, starting at 1. |
| `per_page` | integer | no | Rows per page, 1 to 100. |

```bash
curl "https://app.klctai.com/api/contacts" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "contacts": [
        {
            "contact_id": 90412,
            "name": "Dana",
            "last_name": "Cole",
            "email": "dana@acme.example",
            "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
    }
}
```

### Get a contact

`GET /contacts/{id}`

Operation id: `getContact`. Scope: `contacts:read`.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Contact id. |

```bash
curl "https://app.klctai.com/api/contacts/123" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "contact": {
        "contact_id": 90412,
        "name": "Dana",
        "last_name": "Cole",
        "email": "dana@acme.example",
        "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"
    }
}
```

### Update a contact

`PATCH /contacts/{id}`

Operation id: `updateContact`. Scope: `contacts:write`.

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.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Contact id. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no | First name. Cannot be blank. |
| `last_name` | string | no | Last name. |
| `email` | string | no | Email address. |
| `phone` | string | no | Phone in any common format; stored as E.164. An unparseable value is rejected. |
| `company` | string | no | Company. |
| `website` | string | no | Website. |
| `country` | string | no | Country. |
| `metadata` | object | no | Merged into the existing metadata. |

```bash
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`:

```json
{
    "contact": {
        "contact_id": 90412,
        "name": "Dana",
        "last_name": "Cole",
        "email": "dana@acme.example",
        "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"
    }
}
```

### Create one or many contacts

`POST /contacts/create`

Operation id: `createContacts`. Scope: `contacts:write`.

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.

**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 | no | Company. |
| `website` | string | no | Website. |
| `country` | string | no | Country. |
| `metadata` | object | no | Your own key/value data. |
| `contacts` | array | no | Alternative to the top-level fields: several contacts at once. |

```bash
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":"dana@acme.example","phone":"+15551234567","contacts":[]}'
```

Response `200`:

```json
{
    "contacts": {
        "contact_id": 90412,
        "name": "Dana"
    }
}
```

### Create or update contacts by email

`POST /platform/contacts/sync`

Operation id: `syncContacts`. Scope: `contacts:write`.

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.

**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 | no | First name. |
| `contacts[].last_name` | string | no | Last name. |
| `contacts[].phone` | string | no | Any common phone format; ignored if it cannot be parsed. |
| `contacts[].company` | string | no | Company. |
| `contacts[].tags` | array | no | Up to 10 tags, 50 characters each. |
| `contacts[].metadata` | object | no | Key/value data, merged. |

```bash
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":"dana@acme.example","name":"Dana","tags":[]}]}'
```

Response `200`:

```json
{
    "created": 1,
    "updated": 0,
    "errors": []
}
```

## Contact lists

Groups of contacts used as audiences for campaigns and AI call batches. These routes need a valid token but no specific scope.

### List contact lists

`GET /contactslists/list`

Operation id: `listContactLists`. Any valid token.

```bash
curl "https://app.klctai.com/api/contactslists/list" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "data": [
        {
            "contacts_list_id": 12,
            "name": "Spring leads",
            "target_industry": "Dental"
        }
    ]
}
```

### Create a contact list

`POST /contactslists/create`

Operation id: `createContactList`. Any valid token.

**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 | no | Your own key/value data. |

```bash
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`:

```json
{
    "contact_list_id": 12
}
```

### Rename or edit a contact list

`POST /contactslists/{contactList}/update`

Operation id: `updateContactList`. Any valid token.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `contactList` | integer | yes | Contact list id. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | List name. |
| `target_industry` | string | no | Industry. |
| `metadata` | object | no | Merged into existing metadata. |

```bash
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`:

```json
{
    "contact_list_id": 12
}
```

### Delete a contact list

`POST /contactslists/{contactList}/delete`

Operation id: `deleteContactList`. Any valid token.

Deletes the list, not the contacts in it.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `contactList` | integer | yes | Contact list id. |

```bash
curl -X POST "https://app.klctai.com/api/contactslists/123/delete" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "success": "Contact list deleted with success!"
}
```

### List the contacts in a list

`GET /contactslists/{contactList}/get_contacts`

Operation id: `listContactListMembers`. Any valid token.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `contactList` | integer | yes | Contact list id. |

```bash
curl "https://app.klctai.com/api/contactslists/123/get_contacts" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "data": [
        {
            "contact_id": 90412,
            "name": "Dana"
        }
    ]
}
```

### Create contacts and put them in a list

`POST /contactslists/{contactList}/create_sync_contacts`

Operation id: `createAndSyncListContacts`. Any valid token.

Creates each contact (or updates the existing one with the same phone or email) and adds it to the list.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `contactList` | integer | yes | Contact list id. |

**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 | no | Last name. |
| `contacts[].phone` | string | no | Phone. |
| `contacts[].company` | string | no | Company. |
| `contacts[].website` | string | no | Website. |
| `contacts[].country` | string | no | Country. |
| `contacts[].metadata` | object | no | Key/value data. |

```bash
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":"dana@acme.example","phone":"+15551234567"}]}'
```

Response `200`:

```json
{
    "success": "The contacts where synced with success!"
}
```

### Set the members of a list

`POST /contactslists/{contactList}/sync_contacts`

Operation id: `syncListContacts`. Any valid token.

Sets the list membership from `contacts_ids`.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `contactList` | integer | yes | Contact list id. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | List name (this endpoint requires it). |
| `contacts_ids` | array | no | Contact ids that should be in the list. |

```bash
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`:

```json
{
    "success": "The contacts where synced with success!"
}
```

### Remove contacts from a list

`POST /contactslists/{contactList}/remove_contacts`

Operation id: `removeListContacts`. Any valid token.

Removes membership only. The contacts themselves are kept.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `contactList` | integer | yes | Contact list id. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `contacts_ids` | array | yes | Contact ids to remove. |

```bash
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`:

```json
{
    "success": "Contacts removed"
}
```

## Calls

Calls the AI agents and your team made or received.

### List calls

`GET /calls`

Operation id: `listCalls`. Scope: `calls:read`.

Newest first, with outcome and duration.

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer | no | Page number, starting at 1. |
| `per_page` | integer | no | Rows per page, 1 to 100. |

```bash
curl "https://app.klctai.com/api/calls" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "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
    }
}
```

## 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.

### List tickets

`GET /tickets`

Operation id: `listTickets`. Scope: `tickets:read`.

Newest first. Filter by status, contact, search text, or what changed since a time (handy for incremental sync).

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer | no | Page number, starting at 1. |
| `per_page` | integer | no | Rows per page, 1 to 100. |
| `status` | string | no | One exact status, or `unresolved` for every status that still needs work. |
| `q` | string | no | Matches the subject or ticket number. |
| `contact_id` | integer | no | Only tickets for this contact. |
| `updated_since` | string | no | Only tickets changed at or after this time. |

```bash
curl "https://app.klctai.com/api/tickets?status=unresolved" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "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": "dana@acme.example",
                "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
    }
}
```

### Get a ticket with its conversation

`GET /tickets/{id}`

Operation id: `getTicket`. Scope: `tickets:read`.

Includes the customer-visible message thread. Internal notes and staff details beyond a display name are left out.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Ticket id. |

```bash
curl "https://app.klctai.com/api/tickets/123" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "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": "dana@acme.example",
            "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"
            }
        ]
    }
}
```

### Create a ticket

`POST /tickets`

Operation id: `createTicket`. Scope: `tickets:write`.

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.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `subject` | string | yes | Subject line, up to 255 characters. |
| `description` | string | no | What happened, up to 20000 characters. |
| `priority` | integer | no | 0 to 4. Defaults to 0. |
| `contact_id` | integer | no | Requester. Must be a contact in this account. |
| `due_at` | string | no | When it should be handled by. |

```bash
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`:

```json
{
    "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": "dana@acme.example",
            "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"
            }
        ]
    }
}
```

### Update a ticket

`PATCH /tickets/{id}`

Operation id: `updateTicket`. Scope: `tickets:write`.

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.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Ticket id. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `subject` | string | no | New subject. |
| `description` | string | no | New description. |
| `priority` | integer | no | 0 to 4. |
| `status` | string | no | A valid ticket status, for example open, pending, resolved, closed. |
| `due_at` | string | no | New due date. |

```bash
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`:

```json
{
    "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": "dana@acme.example",
            "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"
            }
        ]
    }
}
```

### Add an internal note

`POST /tickets/{id}/notes`

Operation id: `addTicketNote`. Scope: `tickets:write`.

A note for your team. It is recorded in the ticket history and is never shown to or sent to the customer.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Ticket id. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `body` | string | yes | Note text, up to 10000 characters. |

```bash
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`:

```json
{
    "note": {
        "id": 5521,
        "body": "Reproduced on iOS 19. Passing to engineering.",
        "created_at": "2026-10-14T10:02:00Z"
    }
}
```

### Reply to the customer

`POST /tickets/{id}/replies`

Operation id: `replyToTicket`. Scope: `tickets:write`.

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.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Ticket id. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `body` | string | yes | Reply text, up to 16000 characters. |

```bash
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`:

```json
{
    "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"
    }
}
```

## Chat conversations

Live chat and messaging threads (web chat, WhatsApp, Instagram, email). Needs the Support module on the account.

### List chat conversations

`GET /conversations`

Operation id: `listConversations`. Scope: `conversations:read`.

Most recently active first.

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer | no | Page number, starting at 1. |
| `per_page` | integer | no | Rows per page, 1 to 100. |
| `status` | string | no | One exact status, or `unresolved` for everything not resolved or closed. |
| `contact_id` | integer | no | Only conversations with this contact. |
| `updated_since` | string | no | Only conversations changed at or after this time. |

```bash
curl "https://app.klctai.com/api/conversations?status=unresolved" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "conversations": [
        {
            "id": 7781,
            "status": "open",
            "handling_mode": "ai",
            "channel": "web",
            "subject": null,
            "priority": "normal",
            "contact": {
                "contact_id": 90412,
                "name": "Dana",
                "last_name": "Cole",
                "email": "dana@acme.example",
                "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
    }
}
```

### Get a conversation with its messages

`GET /conversations/{id}`

Operation id: `getConversation`. Scope: `conversations:read`.

Reading a conversation does not mark it read for your team.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Conversation id. |

```bash
curl "https://app.klctai.com/api/conversations/123" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "conversation": {
        "id": 7781,
        "status": "open",
        "handling_mode": "ai",
        "channel": "web",
        "subject": null,
        "priority": "normal",
        "contact": {
            "contact_id": 90412,
            "name": "Dana",
            "last_name": "Cole",
            "email": "dana@acme.example",
            "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"
            }
        ]
    }
}
```

### Send a reply in a conversation

`POST /conversations/{id}/messages`

Operation id: `replyToConversation`. Scope: `conversations:write`.

Sends a real message to the customer on the conversation's channel, and takes the conversation over from the AI (`handling_mode` becomes `human`).

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Conversation id. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `body` | string | yes | Message text, up to 16000 characters. |

```bash
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`:

```json
{
    "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"
}
```

### Resolve a conversation

`POST /conversations/{id}/resolve`

Operation id: `resolveConversation`. Scope: `conversations:write`.

Marks the conversation resolved. The customer may be asked for a rating, depending on the widget settings.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Conversation id. |

```bash
curl -X POST "https://app.klctai.com/api/conversations/123/resolve" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "conversation": {
        "status": "resolved",
        "id": 7781,
        "handling_mode": "ai",
        "channel": "web",
        "subject": null,
        "priority": "normal",
        "contact": {
            "contact_id": 90412,
            "name": "Dana",
            "last_name": "Cole",
            "email": "dana@acme.example",
            "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"
    }
}
```

## 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.

### List changelog entries

`GET /changelog`

Operation id: `listChangelog`. Scope: `changelog:read`.

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer | no | Page number, starting at 1. |
| `per_page` | integer | no | Rows per page, 1 to 100. |
| `status` | string (`draft`, `published`) | no | Only drafts or only published entries. |

```bash
curl "https://app.klctai.com/api/changelog" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "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
    }
}
```

### Get a changelog entry

`GET /changelog/{id}`

Operation id: `getChangelogEntry`. Scope: `changelog:read`.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Entry id. |

```bash
curl "https://app.klctai.com/api/changelog/123" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "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>"
    }
}
```

### Create a changelog entry

`POST /changelog`

Operation id: `createChangelogEntry`. Scope: `changelog:write`.

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.

**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 | no | Version label, up to 50 characters. |
| `category_id` | integer | no | One of this portal's categories. |
| `labels` | array | no | Up to 10 labels, 40 characters each. |
| `status` | string (`draft`, `published`) | no | Defaults to draft on create. |
| `published_at` | string | no | When it goes live. Defaults to now when publishing. |

```bash
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`:

```json
{
    "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>"
    }
}
```

### Update a changelog entry

`PATCH /changelog/{id}`

Operation id: `updateChangelogEntry`. Scope: `changelog:write`.

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.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | integer | yes | Entry id. |

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `title` | string | no | Title, 3 to 255 characters. |
| `content` | string | no | Body up to 60000 characters. Plain text, Markdown or HTML; HTML is sanitized. |
| `version` | string | no | Version label, up to 50 characters. |
| `category_id` | integer | no | One of this portal's categories. |
| `labels` | array | no | Up to 10 labels, 40 characters each. |
| `status` | string (`draft`, `published`) | no | Defaults to draft on create. |
| `published_at` | string | no | When it goes live. Defaults to now when publishing. |

```bash
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`:

```json
{
    "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>"
    }
}
```

## 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.

### List feedback posts

`GET /feedback`

Operation id: `listFeedback`. Scope: `feedback:read`.

Most voted first.

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `page` | integer | no | Page number, starting at 1. |
| `per_page` | integer | no | Rows per page, 1 to 100. |
| `status` | string | no | Board status slug. |
| `category_id` | integer | no | Only posts in this category. |
| `q` | string | no | Matches the title or text. |

```bash
curl "https://app.klctai.com/api/feedback?status=planned" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "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
    }
}
```

### Get the roadmap

`GET /roadmap`

Operation id: `getRoadmap`. Scope: `feedback:read`.

The roadmap columns in display order, each with its posts. Not paginated.

```bash
curl "https://app.klctai.com/api/roadmap" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "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"
                }
            ]
        }
    ]
}
```

## 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.

### Forward a product event to start a sequence

`POST /integrations/sprwt/events`

Operation id: `sendEvent`. Scope: `sequences:trigger`.

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.

**Body (JSON)**

| Field | Type | Required | Description |
|---|---|---|---|
| `trigger` | string | yes | Event key a sequence trigger listens for. |
| `external_ref` | string | no | 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 | no | Last name. |
| `contact.email` | string | no | Email. Either email or phone is required. |
| `contact.phone` | string | no | Phone. |
| `metadata` | object | no | Extra data stored on the contact. |
| `attribution` | object | no | utm_source, utm_medium, utm_campaign, utm_content, utm_term, fbclid, gclid, klct_ad. |
| `kpi` | object | no | Revenue for purchase events. |
| `kpi.value_minor` | integer | no | Order value in minor units (cents). |
| `kpi.currency` | string | no | ISO currency code. |

```bash
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":"dana@acme.example"},"kpi":{"value_minor":4900,"currency":"USD"}}'
```

Response `200`:

```json
{
    "status": "ok",
    "enrolled": 1
}
```

### List your webhook subscriptions

`GET /oauth-platform/webhooks`

Operation id: `listWebhooks`. Scope: `webhooks:manage`.

Only subscriptions created by the calling connection are visible. Requires an OAuth connection, not a manual API key.

```bash
curl "https://app.klctai.com/api/oauth-platform/webhooks" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "webhooks": [
        {
            "id": 7,
            "event_type": "contact.created",
            "target_url": "https://acme.example/hooks/klct",
            "is_active": true,
            "created_at": "2026-10-01T12:00:00Z"
        }
    ]
}
```

### Subscribe to an event

`POST /oauth-platform/webhooks`

Operation id: `createWebhook`. Scope: `webhooks:manage`.

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.

**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. |

```bash
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`:

```json
{
    "id": 7,
    "event_type": "contact.created",
    "target_url": "https://acme.example/hooks/klct",
    "signing_secret": "3f8c0b6d1e..."
}
```

### Remove a webhook subscription

`DELETE /oauth-platform/webhooks/{webhook}`

Operation id: `deleteWebhook`. Scope: `webhooks:manage`.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `webhook` | integer | yes | Subscription id. |

```bash
curl -X DELETE "https://app.klctai.com/api/oauth-platform/webhooks/123" \
  -H "Authorization: Bearer $KLCT_API_KEY"
```

Response `200`:

```json
{
    "status": "ok"
}
```

## Meetings

Bookable meeting types, for embedding a booking link in another product.

### List an account's bookable meetings

`GET /{account}/meetings`

Operation id: `listMeetings`. Any valid token.

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.

**Path parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `account` | integer | yes | KLCT account id. |

```bash
curl "https://app.klctai.com/api/123/meetings" \
  -H "Authorization: Bearer $KLCT_APP_API_TOKEN"
```

Response `200`:

```json
{
    "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"
        }
    ]
}
```

