Klantly Developers

Appointments from your own planning

Put appointments from your own planning software into the Klantly calendar, keep them in sync and let Klantly send the customer a confirmation.

Do you plan in a system of your own, for example a planning board for your technicians? With the API you put those appointments into the Klantly calendar. You then see them on the customer and on your pipeline board, and your customer gets the same confirmations and reminders as for an appointment you make in Klantly itself.

What you need

An API key with these scopes:

Scope What for
customers.read Look up the customer by email address.
customers.write Create a customer that does not exist yet.
appointments.read List appointments and free time slots.
appointments.write Schedule, reschedule, cancel and complete appointments.
appointments.send Email the customer a confirmation, reschedule, cancellation or reminder.

The Appointments feature must be enabled for your company.

Step 1: the customer

An appointment always belongs to a customer; name, email address and phone come from them. Look up the customer with filter[email] or create them, as in Website form to lead. Store their id in your own system, so you do not have to look them up again next time.

Step 2: schedule the appointment

cURL
curl -X POST "https://app.klantly.com/api/v1/appointments" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: planning-4711" \
  -d '{"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70", "title": "Measure veranda", "starts_at": "2026-10-06T09:00:00+02:00", "ends_at": "2026-10-06T10:30:00+02:00", "location": "Dorpsstraat 12, Utrecht"}'
  • Always send times with a time zone, such as +02:00 or Z. The response has them in UTC.
  • If you leave out ends_at, the appointment lasts as long as the appointment type in appointment_type_id, or otherwise the default duration from your appointment settings.
  • Put the id from your own planning in the Idempotency-Key. If your server retries after a timeout, there is still only one appointment.
  • Store the id from the response next to your own id. You need it to update the appointment later.

A new appointment is confirmed. If it still needs to be confirmed, send "status": "pending". Klantly does not check availability when scheduling: your planning is leading.

Note

The API does not email the customer when scheduling. You do that on purpose, in step 4. Note: if your company has automations on "appointment scheduled" (for example a WhatsApp confirmation), those do run, just like for an appointment in the calendar. Switch them off for a moment when you import your planning for the first time.

Step 3: reschedule, cancel and complete

If the time changes in your planning, send only the new time:

cURL
curl -X PATCH "https://app.klantly.com/api/v1/appointments/0b6f3a52-7c1d-4e8a-9b2f-5d4c3b2a1f09" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"starts_at": "2026-10-08T13:00:00+02:00"}'

Without ends_at the duration stays the same, and rescheduled_at shows that the appointment was rescheduled. If an appointment is called off, cancel it instead of deleting it: that way it stays visible on the customer.

cURL
curl -X POST "https://app.klantly.com/api/v1/appointments/0b6f3a52-7c1d-4e8a-9b2f-5d4c3b2a1f09/cancel" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Customer is ill"}'

Once the visit has taken place, complete the appointment with POST /appointments/{id}/complete. If your lead conversion setting is "appointment completed", a lead becomes a customer, just like in the calendar.

Step 4: inform the customer

cURL
curl -X POST "https://app.klantly.com/api/v1/appointments/0b6f3a52-7c1d-4e8a-9b2f-5d4c3b2a1f09/notify" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: planning-4711-confirmation" \
  -d '{"message": "confirmation"}'

Klantly sends the email with the template you set up in Klantly. The message must match the status of the appointment:

message Allowed with status
confirmation confirmed
reschedule pending or confirmed
cancellation cancelled
reminder confirmed

If it does not match, the appointment has no date, the customer has no email address or your company has switched the template off, you get 409 with the code invalid_state_transition. detail says why.

The other way round: bookings from Klantly

Customers can also book themselves through your booking page. To see those in your planning, listen with a webhook to appointment.created, appointment.updated, appointment.confirmed, appointment.cancelled, appointment.completed and appointment.deleted. A status change comes as its own event, not as appointment.updated. Every event contains the whole appointment.

Those events also arrive for the appointments you scheduled yourself through the API. Recognise them by the id you stored in step 2, otherwise they end up in your planning twice.

Look up free time slots

To see in your own system where Klantly still has room, request the free time slots:

cURL
curl --globoff "https://app.klantly.com/api/v1/availability?date_from=2026-10-06&date_to=2026-10-10&duration_minutes=90" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"

These are nearly the same time slots as on your booking page: working hours, breaks, blocked days, how far ahead bookings are allowed and the appointments already booked all count. Only your Google Calendar does not count here, and the times apply to the whole company, not per employee. You request at most 31 days at a time.

All together

PHP
use GuzzleHttp\Client;

/**
 * $klantly is a Guzzle client with the base URL and your API key, $job an appointment from your own
 * planning. Returns the id of the appointment in Klantly: store it with the job.
 */
function syncAppointment(Client $klantly, array $job): string
{
    $body = [
        'title' => $job['title'],
        'starts_at' => $job['start']->format(DATE_ATOM),
        'ends_at' => $job['end']->format(DATE_ATOM),
        'location' => $job['address'],
    ];

    // New: schedule it and send the customer a confirmation.
    if ($job['klantly_id'] === null) {
        $appointment = json_decode((string) $klantly->post('appointments', [
            'headers' => ['Idempotency-Key' => "planning-{$job['id']}"],
            'json' => $body + ['customer_id' => $job['klantly_customer_id']],
        ])->getBody(), true)['data'];

        $klantly->post("appointments/{$appointment['id']}/notify", [
            'headers' => ['Idempotency-Key' => "planning-{$job['id']}-confirmation"],
            'json' => ['message' => 'confirmation'],
        ]);

        return $appointment['id'];
    }

    // Existing: update it, and tell the customer when the time has changed.
    $klantly->patch("appointments/{$job['klantly_id']}", ['json' => $body]);

    if ($job['time_changed']) {
        $klantly->post("appointments/{$job['klantly_id']}/notify", [
            'json' => ['message' => 'reschedule'],
        ]);
    }

    return $job['klantly_id'];
}
Node.js
const BASE_URL = 'https://app.klantly.com/api/v1';

async function klantly(method, path, { body, idempotencyKey } = {}) {
  const response = await fetch(`${BASE_URL}/${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
      ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  const json = await response.json();
  if (!response.ok) throw new Error(json.detail ?? json.title);
  return json;
}

// job is an appointment from your own planning; start and end are ISO 8601 with a time zone.
// Returns the id of the appointment in Klantly: store it with the job.
export async function syncAppointment(job) {
  const body = { title: job.title, starts_at: job.start, ends_at: job.end, location: job.address };

  // New: schedule it and send the customer a confirmation.
  if (!job.klantlyId) {
    const { data } = await klantly('POST', 'appointments', {
      body: { ...body, customer_id: job.klantlyCustomerId },
      idempotencyKey: `planning-${job.id}`,
    });
    await klantly('POST', `appointments/${data.id}/notify`, {
      body: { message: 'confirmation' },
      idempotencyKey: `planning-${job.id}-confirmation`,
    });
    return data.id;
  }

  // Existing: update it, and tell the customer when the time has changed.
  await klantly('PATCH', `appointments/${job.klantlyId}`, { body });
  if (job.timeChanged) {
    await klantly('POST', `appointments/${job.klantlyId}/notify`, { body: { message: 'reschedule' } });
  }
  return job.klantlyId;
}

Handling errors

  • 422 with validation_failed: for example a time without a time zone. errors says per field what is wrong.
  • 409 with invalid_state_transition: for example completing a cancelled appointment, or a message that does not match the status.
  • 429 with rate_limited: wait the number of seconds in Retry-After and try again. When you import your planning for the first time, spread the requests over time. See Rate limits.

Last updated on September 15, 2026