Documentation · Chapter 10

Kolio REST API and webhooks

The Kolio REST API (Integrations API v1) lets an external system — an ERP, a webshop, Zapier or your own script — read and change the bookings, contacts and tasks of your Kolio account. An API key kolio_sk_… opens access, scopes limit what it may do, webhooks push events to your URL. This guide covers keys, authentication, endpoints and signatures.

By the Kolio teamupdated 12 min read25 min

What does the Kolio REST API do?

Kolio REST API is an HTTP/JSON interface under the base URL https://kolio.si/api/integrations/v1 through which an external system reads and changes the bookings (calendar), CRM contacts and tasks of one Kolio account; it authenticates with an API key that determines both the account (tenant) and the permitted scope.

All three entry points — the application UI, the REST API and the MCP server — write through the same business cores. A booking created over the API goes through the same location opening hours, free performer and overlap checks as one your team enters in the calendar; a task gets the same timeline as in the CRM. An integration therefore cannot create a record the application itself would refuse.

What Integrations API v1 covers
AreaWhat you can doScope
Bookings (calendar)list by period, single booking, create, move/change, cancel, permanent deletebookings:read, bookings:write
Contacts (CRM)list and search, single contact with notes, create, change with a note, deletecontacts:read, contacts:write
Tasks (CRM)filtered list, single task with timeline, create, change with a comment, deletetasks:read, tasks:write
Webhookspush notifications about events to your URL, signed with HMAC-SHA256no scope — configured in the app

Conversations, reports and the knowledge base have their own scopes, but REST v1 has no endpoints for them. Locations (branches with opening hours) are returned by GET /locations with the services:read scope; services are available through the list_services tool on the MCP server.

How do I create an API key?

An API key is created by a member with the Settings permission on the Settings → MCP sources and integrations page, in the API keys section; the full key is shown only once, and the database keeps only its SHA-256 hash and a visible prefix.

  1. Open the integration settings

    In the menu choose Settings → MCP sources and integrations. The upper part of the page, Kolio as a source for external systems, contains the API keys and Webhooks sections.

    Settings → MCP sources and integrations
  2. Name the key

    In Key name enter which system will use it (e.g. ERP integration). The name goes into the audit trail: in a task timeline and in contact notes a change made over the API appears as api:ERP integration, not as a person's name.

  3. Limit the scopes

    Under Scopes tick the scopes the integration needs, e.g. contacts:read and bookings:write. A key never exceeds your own permissions in this account: scopes you lack the matching permission for are disabled, and if they are requested outside the form Kolio refuses to create the key and lists the rejected scopes. If you tick nothing the key gets every scope you are allowed to issue — always as an explicit list, never *. We recommend the smallest set the integration needs.

  4. Create and store the key

    Click Create key. A Key created box appears with the warning Save it now — it is shown only once. Copy the whole kolio_sk_… string into your system's secret store. After a page refresh only the beginning of the key (the prefix) remains visible.

  5. Verify the key

    Call the GET /ping endpoint (example in the next section). The response returns the account id and the key's scopes.

How do I authenticate?

Send the key in the Authorization: Bearer kolio_sk_… header or in the X-API-Key: kolio_sk_… header; the account (tenant) is always derived from the key and never from the request body or URL, so one key can never reach another account's data.

curl -s https://kolio.si/api/integrations/v1/ping \
  -H "Authorization: Bearer kolio_sk_…"

# response
{
  "ok": true,
  "tenantId": "…",
  "scopes": ["contacts:read", "bookings:write"],
  "timestamp": "2026-09-15T08:12:03.512Z"
}
Checking the key. /ping is the only endpoint whose response is not wrapped in data.

All other endpoints return { "data": … } on success and { "error": "code", "message": "explanation in Slovenian" } on failure. The error code is stable and meant for program logic; the message is meant for people.

General errors returned by every endpoint
HTTPCode (error)When
401unauthorizedthe key is missing, invalid or revoked
403insufficient_scopethe key lacks the scope the endpoint requires (the message names the missing scope)
403plan_featurethe account's plan does not include the REST API (for the MCP server: the MCP server feature); the message names the plan the feature starts at
403tenant_inactivethe account's subscription or trial is no longer active — the key is valid but refused until renewal
400invalid_jsonthe request body is not valid JSON
422invalid_bodythe body does not match the schema; the message names the first invalid field
404not_foundno record with that id exists in this account
422no_changesa PATCH contains no field to change

Scopes and permissions

Every API key scope maps to an existing member permission in Kolio, so a key never opens an area the application itself does not know; scopes are exact strings with no hierarchy — contacts:write does not include contacts:read.

Scopes of a Kolio API key
ScopeWhat it coversMatching member permissionChanges data
bookings:readreading bookings (calendar)Bookingsno
bookings:writecreating, changing, cancelling and deleting bookingsBookingsyes
services:readreading services (MCP) and locations (GET /locations)Servicesno
contacts:readreading contacts and their notesContactsno
contacts:writecreating, changing, annotating and deleting contactsContactsyes
tasks:readreading tasks and their timelineTasksno
tasks:writecreating, changing, commenting on and deleting tasksTasksyes
conversations:readconversations and transcriptsConversationsno
reports:readreportsReportsno
knowledge:readknowledge baseKnowledge baseno
email:writesending email on your behalfEmail marketingyes

REST v1 currently has no endpoints for conversations:read, reports:read, knowledge:read or email:write. email:write is deliberately not part of the default MCP connector set.

Which endpoints are available?

Integrations API v1 has eight paths: ping, locations plus two each for bookings, contacts and tasks — the collection (GET list, POST create) and the single record (GET, PATCH, DELETE); lists are limited by the limit parameter (default 50, maximum 200).

Kolio REST API v1 endpoints (base URL https://kolio.si/api/integrations/v1)
Method and pathWhat it doesScopeParameters / fields
GET /pingchecks the keyany—
GET /locationsactive locations with address and opening hours (hours: weekday 0 = Sunday … 6 = Saturday, startTime/endTime HH:MM in the account's time zone)services:readincludeInactive=1
GET /bookingslist of bookings, newest first; each booking carries locationId and locationNamebookings:readfrom, to (ISO 8601), limit
POST /bookingscreates a booking; fires booking.createdbookings:writeslotStart* and one of serviceId / durationMin / slotEnd; contactId, title, notes, assigneeEmail, locationId (required with several locations, otherwise the core picks the location where the slot fits)
GET /bookings/{id}single bookingbookings:read—
PATCH /bookings/{id}move, change or cancel (status: cancelled fires booking.cancelled)bookings:writeslotStart, slotEnd, serviceId, contactId, title, notes, assigneeEmail, locationId, status
DELETE /bookings/{id}permanently deletes a bookingbookings:write—
GET /contactslist / search contactscontacts:readq, email, phone, externalId, limit
POST /contactscreates a contact; fires contact.createdcontacts:writeat least one of firstName, lastName, email, phone; company, notes, tags, externalId, gdprConsent, marketingConsent
GET /contacts/{id}contact with notes (notes[])contacts:read—
PATCH /contacts/{id}partial change; note appends a notecontacts:writesame fields as POST + note
DELETE /contacts/{id}permanently deletes a contact (e.g. a GDPR request)contacts:write—
GET /taskslist of taskstasks:readstatus, assigneeEmail, contactId, q, limit
POST /taskscreates a task; fires task.createdtasks:writetitle*; description, assigneeEmail, contactId, priority, dueAt, status
GET /tasks/{id}task with timeline (events[])tasks:read—
PATCH /tasks/{id}partial change; comment adds a comment; fires task.updatedtasks:writesame fields as POST + comment
DELETE /tasks/{id}permanently deletes a task with its timelinetasks:write—

An asterisk marks a required field. In a PATCH, fields you do not send stay unchanged; null (for tasks also an empty string) clears a field.

Bookings

curl -s -X POST https://kolio.si/api/integrations/v1/bookings \
  -H "Authorization: Bearer kolio_sk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "slotStart": "2026-09-22T09:00",
    "serviceId": "<service id>",
    "contactId": "<contact id>",
    "locationId": "<location id from GET /locations>",
    "notes": "Booking from the webshop"
  }'

# 201
{ "data": { "id": "…", "slotStart": "2026-09-22T07:00:00.000Z",
            "slotEnd": "2026-09-22T07:30:00.000Z", "status": "confirmed",
            "serviceId": "…", "contactId": "…", "locationId": "…", "locationName": "Ljubljana",
            "source": "api", "…": "…" } }

# 422 — outside the location's opening hours, no free performer or assignee is not a member
{ "error": "booking_rejected", "message": "Termin je izven delovnega časa lokacije." }
Creating a booking for a service; the duration is taken from the service. The response is 201.

REST v1 does not offer a free-slot list; free times are returned by the find_free_slots tool on the MCP server. You can also check occupancy with GET /bookings?from=…&to=… together with the locations' opening hours from GET /locations, which you set in calendar and services. Writes over the API are strict: a slot outside the location's opening hours or without a free performer returns 422 booking_rejected; only the team can enter exceptions in the calendar.

Errors of the booking endpoints
HTTPCodeWhen
422missing_durationPOST without serviceId, durationMin and slotEnd
422invalid_slotslotStart or slotEnd is not a valid ISO 8601 time
422invalid_rangefrom or to on GET is not a valid time
422booking_rejectedthe core refused the booking: slot taken, unknown assignee, invalid service

Contacts

curl -s -X POST https://kolio.si/api/integrations/v1/contacts \
  -H "X-API-Key: kolio_sk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Ana",
    "lastName": "Novak",
    "email": "ana.novak@example.com",
    "phone": "+38640123456",
    "externalId": "erp-10482",
    "tags": ["webshop"],
    "gdprConsent": true
  }'

# 201
{ "data": { "id": "…", "firstName": "Ana", "lastName": "Novak",
            "email": "ana.novak@example.com", "externalId": "erp-10482", "…": "…" } }

# 422
{ "error": "missingFields",
  "message": "Kontakt potrebuje vsaj eno od polj: ime, priimek, telefon ali e-pošta." }
Creating a contact with consent. gdprConsent: true records consent with today's date and the source api.

Check whether the contact already exists before creating it: GET /contacts?email=…, ?phone=… or ?externalId=… return exact matches, while ?q=… searches first name, last name, email, phone and company. The externalId field is meant for the customer's id in your system (ERP, webshop).

Tasks

# POST — new task
curl -s -X POST https://kolio.si/api/integrations/v1/tasks \
  -H "Authorization: Bearer kolio_sk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Call the customer about quote 2026-118",
    "contactId": "<contact id>",
    "assigneeEmail": "sales@your-company.com",
    "priority": 2,
    "dueAt": "2026-09-18T12:00"
  }'

# 201
{ "data": { "id": "tsk_…", "title": "Call the customer …", "status": "nova",
            "priority": 2, "dueAt": "2026-09-18T10:00:00.000Z", "…": "…" } }

# PATCH — complete the task with a comment
curl -s -X PATCH https://kolio.si/api/integrations/v1/tasks/tsk_… \
  -H "Authorization: Bearer kolio_sk_…" \
  -H "Content-Type: application/json" \
  -d '{ "status": "zakljucena", "comment": "The customer confirmed the quote." }'

# 422 — unknown assignee
{ "error": "unknownAssignee", "message": "…" }
Creating a task and then changing its status with a comment. Both changes are written to the timeline with the author api: + key name.
Task field values
FieldAllowed valuesNote
statusnova (new), v_teku (in progress), v_cakanju (waiting), zakljucena (done), arhivirana (archived)to keep the trail prefer arhivirana over DELETE
priorityinteger 0–3values outside the range are clamped
assigneeEmailemail of an active member of the accountnull or an empty string unassigns the task; an unknown address → unknownAssignee
dueAtISO 8601 or a plain date YYYY-MM-DDnull or an empty string removes the due date; invalid → invalid_due

How does the API interpret time?

All input times are ISO 8601; a string with an offset or Z is taken literally, a string without an offset (2026-09-22T09:00) is read as local time in your account's time zone (Settings → General, default Europe/Ljubljana), and a plain date (2026-09-22) as midnight of that day in the same zone.

Responses return times as UTC instants with a trailing Z (e.g. 2026-09-22T07:00:00.000Z). The server runs in UTC; without this rule a booking sent as 09:00 without an offset would silently shift by one or two hours. The rule applies to slotStart, slotEnd, from, to and dueAt.

How do webhooks work?

A webhook is an HTTP POST that Kolio sends to your URL when an event happens, with the JSON envelope { event, tenantId, timestamp, data } signed with HMAC-SHA256 (RFC 2104) in the X-Kolio-Signature header (format sha256= + hex HMAC); the event time is in the X-Kolio-Timestamp header and in the body's timestamp field, so the signature is bound to time. Delivery is one attempt, best effort (no retries).

Kolio settings MCP sources and integrations: the Webhooks section with the URL and Events fields and the Add webhook button, below it the Add server form for external MCP sources
The Webhooks section on the Settings → MCP sources and integrations page.
  1. Prepare the receiving URL

    On your server prepare a path that accepts a POST with a JSON body and responds in under 10 seconds (Kolio aborts the request after 10 s). The address must start with http:// or https://; use HTTPS in production.

  2. Add the webhook

    In the Webhooks section enter the URL and, under Events, tick the events you want to receive (e.g. booking.created and call.completed). If you tick nothing the webhook receives all events. Click Add webhook.

    Settings → MCP sources and integrations → Webhooks
  3. Verify the signature

    On creation Kolio shows the whsec_… secret in the Webhook secret box — only once; store it in your system's secret store, the list later shows only its first few characters. Kolio signs every request with it: compute HMAC-SHA256 over the raw body and compare the result with the X-Kolio-Signature header (example below). If you also check the event age in the body or the X-Kolio-Timestamp header (e.g. at most 5 minutes), you reject replayed requests too.

  4. Manage the webhook

    The list shows each webhook's state active / inactive; the Disable button pauses delivery, Delete removes it. An inactive webhook receives no events. Rotate secret generates a new secret and shows it once; the old one stops working immediately, so swap it on your server at the same time — events delivered between rotation and the swap fail signature verification.

Kolio webhook events
EventWhen it firesFields in data
booking.createdbooking created over REST or MCPbookingId, contactId, serviceId, slotStart, slotEnd, status
booking.cancelledbooking cancelled over REST (status: cancelled) or MCP (cancel_event)bookingId, contactId, slotStart
contact.createdcontact created over REST or MCPcontactId, firstName, lastName, email, phone
task.createdtask created over REST or MCPtaskId, title, status, assigneeEmail, contactId, dueAt
task.updatedtask changed over REST or MCPsame fields as task.created
call.completeda phone call by the AI agent (inbound) or by a team member over the web phone (direction tells which) has ended; sent once the conversation is savedconversationId, externalId, channel (phone), direction, callerNumber, contactId, bookingId, durationSeconds, summary

Booking, contact and task events are fired by the integration paths (REST and MCP), not by your team's actions in the app — a booking a member enters in the calendar sends no webhook. The exception is call.completed, which every completed phone call sends.

POST /webhook HTTP/1.1
Content-Type: application/json
X-Kolio-Event: booking.created
X-Kolio-Timestamp: 2026-09-15T08:14:22.318Z
X-Kolio-Signature: sha256=3f1a9c…

{
  "event": "booking.created",
  "tenantId": "…",
  "timestamp": "2026-09-15T08:14:22.318Z",
  "data": {
    "bookingId": "…",
    "contactId": "…",
    "serviceId": "…",
    "slotStart": "2026-09-22T07:00:00.000Z",
    "slotEnd": "2026-09-22T07:30:00.000Z",
    "status": "confirmed"
  }
}
The request envelope Kolio sends to your URL. The X-Kolio-Event header carries the event name, X-Kolio-Timestamp the event time (equal to the timestamp field), X-Kolio-Signature the signature.
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyKolioSignature(rawBody, signatureHeader, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}
Verifying the signature: HMAC-SHA256 over the raw body (not over re-serialised JSON), hex encoding, constant-time comparison.

Limits and good practice

Kolio REST API v1 has no idempotency key and no webhook retries, so the integration itself must prevent duplicate records — by checking for existence before creating and by storing the ids Kolio returns.

Limits of Kolio REST API v1
WhatValue
Maximum records in a listlimit default 50, maximum 200; no pagination (cursor/offset) — narrow with from/to or filters
Webhook timeout10 s per request, one attempt
Idempotencynot supported — a repeated POST creates a new record (a booking only if the slot is free)
Attachmentscannot be uploaded over the API; a task timeline returns only attachment metadata and a download path that requires a member session
Key scopesfixed at creation; changing them means a new key
Audit trailevery task change and every contact note made over the API carries the author api: + key name
  • Use a separate key with its own name for each integration — the timeline shows which integration acted, and you can revoke it without stopping the others.
  • Before POST /contacts look the contact up by email, phone or externalId; before POST /bookings read the occupancy of the period and, with several locations, pass locationId.
  • To cancel a booking use PATCH { "status": "cancelled" }, to close a task PATCH { "status": "arhivirana" }; DELETE is permanent and also removes the timeline.
  • Send times with an offset and store the ids from responses (data.id) — they are the only reliable key for a later PATCH.
  • Handle errors by the error field, not by the message text, which is meant for people and may change.
FAQ

Frequently asked questions

Where in Kolio do I create an API key?

A Kolio API key is created by a member with the Settings permission on the Settings → MCP sources and integrations page, in the API keys section: enter a name, optionally scopes (e.g. contacts:read, bookings:write) and click Create key. The kolio_sk_… key is shown only once; the database keeps only a SHA-256 hash.

How do I send the API key in a request?

The Kolio REST API accepts the key in the Authorization: Bearer kolio_sk_… header or in X-API-Key: kolio_sk_…. The account is derived from the key, so there is no account id in the body or URL; a successful response is { "data": … }, an error is { "error": "code", "message": "…" }.

How does Kolio sign a webhook?

Kolio signs every webhook request with HMAC-SHA256 over the raw JSON body using the webhook secret (whsec_…, shown once on creation or rotation) and sends the signature in the X-Kolio-Signature header as sha256= followed by a hex string; the event name is in the X-Kolio-Event header, the event time in the X-Kolio-Timestamp header and the body's timestamp field. The request is sent once, with a 10-second timeout and no retries.

Why does a booking sent at 9:00 show up in Kolio at a different hour?

Kolio reads a time without an offset (2026-09-22T09:00) as local time in the account's time zone (Settings → General, default Europe/Ljubljana) and returns it as a UTC instant with a trailing Z. To be independent of the account setting, send the time with an offset, e.g. 2026-09-22T09:00:00+02:00.

Does the Kolio REST API support an idempotency key or pagination?

Kolio REST API v1 has neither an idempotency key nor pagination: a repeated POST creates a new record, and lists return at most 200 records (default 50). The integration should check for an existing record before creating (GET /contacts?externalId=…) and narrow lists with the from/to, status or q filters.

Can't find the answer?

Write to us or open the chat in the app — the Help button at the bottom of the menu hands the conversation to the Kolio team.

We reply during working hours, usually the same day.

Related