Klantly Developers

API reference

Webhook endpoints

URLs Klantly sends a message to as soon as something changes. Signing and retries are explained in the webhooks guide.

Endpoints

List webhook endpoints

GET /api/v1/webhook-endpoints

All webhook endpoints of your company, oldest first. The secret is never included.

Scope
webhooks.manage — Manage webhook endpoints

Example request

cURL
curl "https://app.klantly.com/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"
PHP
$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.klantly.com/api/v1/',
    'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);

$response = $client->request('GET', 'webhook-endpoints');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/webhook-endpoints', {
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
  },
});

const { data } = await response.json();
Python
import os

import requests

response = requests.get(
    "https://app.klantly.com/api/v1/webhook-endpoints",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
)
data = response.json()["data"]

Response 200

The response is a list with cursor pagination: data contains the objects, meta the pagination.

Example response
{
  "data": [
    {
      "object": "webhook_endpoint",
      "id": "01j7zr8m2k4n6p8r0t2v4w6y8a",
      "url": "https://example.com/webhooks/klantly",
      "description": "Boekhouding",
      "events": [
        "customer.created",
        "deal.won"
      ],
      "status": "active",
      "disabled_reason": null,
      "disabled_at": null,
      "previous_secret_expires_at": null,
      "last_success_at": "2026-09-14T10:15:02Z",
      "last_failure_at": null,
      "created_at": "2026-09-14T10:15:00Z",
      "updated_at": "2026-09-14T10:15:00Z"
    }
  ],
  "meta": {
    "limit": 50,
    "next_cursor": "eyJpZCI6IjlkM2Y2YzFlIn0",
    "prev_cursor": null
  }
}

Possible errors

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Retrieve a webhook endpoint

GET /api/v1/webhook-endpoints/{endpoint}

One endpoint by id, with its status and the time of the last successful and failed delivery.

Scope
webhooks.manage — Manage webhook endpoints

Path parameters

NameTypeDescription
endpoint required string The id of the webhook endpoint.

Example request

cURL
curl "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"
PHP
$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.klantly.com/api/v1/',
    'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);

$response = $client->request('GET', 'webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', {
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
  },
});

const { data } = await response.json();
Python
import os

import requests

response = requests.get(
    "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "webhook_endpoint",
    "id": "01j7zr8m2k4n6p8r0t2v4w6y8a",
    "url": "https://example.com/webhooks/klantly",
    "description": "Boekhouding",
    "events": [
      "customer.created",
      "deal.won"
    ],
    "status": "active",
    "disabled_reason": null,
    "disabled_at": null,
    "previous_secret_expires_at": null,
    "last_success_at": "2026-09-14T10:15:02Z",
    "last_failure_at": null,
    "created_at": "2026-09-14T10:15:00Z",
    "updated_at": "2026-09-14T10:15:00Z"
  }
}

Possible errors

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Create a webhook endpoint

POST /api/v1/webhook-endpoints

Registers an https URL that receives events. The response contains the secret for checking signatures: store it right away, you will not see it again. You can only choose events about data your key can read; * means every event within that, including future ones. Up to 10 endpoints per company.

Scope
webhooks.manage — Manage webhook endpoints

Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.

Body (JSON)

FieldTypeDescription
url required string (uri) The https URL Klantly sends events to. No internal addresses, and only port 443, 80 or 8443. at most 2048 characters
description optional string Your own description, for example what the endpoint is for. can be empty (null) · at most 255 characters
events required array The event types this endpoint receives, or ["*"] for every event the key or user can read.

Example request

cURL
curl -X POST "https://app.klantly.com/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
  -d '{
  "url": "https://example.com/webhooks/klantly",
  "description": "Boekhouding",
  "events": [
    "customer.created",
    "deal.won"
  ]
}'
PHP
$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.klantly.com/api/v1/',
    'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);

$response = $client->request('POST', 'webhook-endpoints', [
    'headers' => [
        'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
    ],
    'json' => [
        'url' => 'https://example.com/webhooks/klantly',
        'description' => 'Boekhouding',
        'events' => [
            0 => 'customer.created',
            1 => 'deal.won',
        ],
    ],
]);

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/webhook-endpoints', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
  },
  body: JSON.stringify({
  "url": "https://example.com/webhooks/klantly",
  "description": "Boekhouding",
  "events": [
    "customer.created",
    "deal.won"
  ]
}),
});

const { data } = await response.json();
Python
import os

import requests

response = requests.post(
    "https://app.klantly.com/api/v1/webhook-endpoints",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
        "Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    },
    json={
        "url": "https://example.com/webhooks/klantly",
        "description": "Boekhouding",
        "events": [
            "customer.created",
            "deal.won"
        ]
    },
)
data = response.json()["data"]

Response 201

Example response
{
  "data": {
    "object": "webhook_endpoint",
    "id": "01j7zr8m2k4n6p8r0t2v4w6y8a",
    "url": "https://example.com/webhooks/klantly",
    "description": "Boekhouding",
    "events": [
      "customer.created",
      "deal.won"
    ],
    "status": "active",
    "disabled_reason": null,
    "disabled_at": null,
    "previous_secret_expires_at": null,
    "last_success_at": "2026-09-14T10:15:02Z",
    "last_failure_at": null,
    "created_at": "2026-09-14T10:15:00Z",
    "updated_at": "2026-09-14T10:15:00Z",
    "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
  }
}

Possible errors

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Update a webhook endpoint

PATCH /api/v1/webhook-endpoints/{endpoint}

Changes only the fields you send. Use status to turn the endpoint off or back on; turning it back on resets the failure count.

Scope
webhooks.manage — Manage webhook endpoints

Send the ETag in If-Match and you will never accidentally overwrite a newer version.

Path parameters

NameTypeDescription
endpoint required string The id of the webhook endpoint.

Body (JSON)

FieldTypeDescription
url optional string (uri) The https URL Klantly sends events to. No internal addresses, and only port 443, 80 or 8443. at most 2048 characters
description optional string Your own description, for example what the endpoint is for. can be empty (null) · at most 255 characters
events optional array The event types this endpoint receives, or ["*"] for every event the key or user can read.
status optional string active or disabled. one of: active, disabled

Example request

cURL
curl -X PATCH "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "events": [
    "*"
  ],
  "status": "active"
}'
PHP
$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.klantly.com/api/v1/',
    'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);

$response = $client->request('PATCH', 'webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', [
    'json' => [
        'events' => [
            0 => '*',
        ],
        'status' => 'active',
    ],
]);

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "events": [
    "*"
  ],
  "status": "active"
}),
});

const { data } = await response.json();
Python
import os

import requests

response = requests.patch(
    "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
    json={
        "events": [
            "*"
        ],
        "status": "active"
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "webhook_endpoint",
    "id": "01j7zr8m2k4n6p8r0t2v4w6y8a",
    "url": "https://example.com/webhooks/klantly",
    "description": "Boekhouding",
    "events": [
      "customer.created",
      "deal.won"
    ],
    "status": "active",
    "disabled_reason": null,
    "disabled_at": null,
    "previous_secret_expires_at": null,
    "last_success_at": "2026-09-14T10:15:02Z",
    "last_failure_at": null,
    "created_at": "2026-09-14T10:15:00Z",
    "updated_at": "2026-09-14T10:15:00Z"
  }
}

Possible errors

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Delete a webhook endpoint

DELETE /api/v1/webhook-endpoints/{endpoint}

Deletes the endpoint. Events that are still on their way are no longer delivered.

Scope
webhooks.manage — Manage webhook endpoints

Path parameters

NameTypeDescription
endpoint required string The id of the webhook endpoint.

Example request

cURL
curl -X DELETE "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"
PHP
$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.klantly.com/api/v1/',
    'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);

$response = $client->request('DELETE', 'webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
  },
});

const { data } = await response.json();
Python
import os

import requests

response = requests.delete(
    "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "note",
    "id": "9d3f7b41-2d6f-4e8c-9b3a-4f5d6e7a8b92",
    "deleted": true
  }
}

Possible errors

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Send a test message

POST /api/v1/webhook-endpoints/{endpoint}/test

Sends the ping event to the endpoint right away, even if it is disabled, and returns the delivery attempt. A test message is not retried.

Scope
webhooks.manage — Manage webhook endpoints

Path parameters

NameTypeDescription
endpoint required string The id of the webhook endpoint.

Example request

cURL
curl -X POST "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/test" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"
PHP
$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.klantly.com/api/v1/',
    'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);

$response = $client->request('POST', 'webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/test');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
  },
});

const { data } = await response.json();
Python
import os

import requests

response = requests.post(
    "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/test",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "webhook_delivery",
    "id": "1834",
    "webhook_endpoint_id": "01j7zr8m2k4n6p8r0t2v4w6y8a",
    "event_id": "evt_01j7zs1a2b3c4d5e6f7g8h9j0k",
    "event_type": "ping",
    "attempt": 1,
    "status": "succeeded",
    "response_status": 200,
    "error": null,
    "duration_ms": 184,
    "created_at": "2026-09-14T10:15:00Z"
  }
}

Possible errors

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Rotate the secret

POST /api/v1/webhook-endpoints/{endpoint}/rotate-secret

Creates a new secret and returns it once. During the overlap Klantly signs with both the old and the new secret, so your receiver can switch over without interruption.

Scope
webhooks.manage — Manage webhook endpoints

Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.

Path parameters

NameTypeDescription
endpoint required string The id of the webhook endpoint.

Body (JSON)

FieldTypeDescription
overlap_hours optional integer How long the old secret keeps working next to the new one: 0, 1, 24 (default) or 168 hours. one of: 0, 1, 24, 168

Example request

cURL
curl -X POST "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/rotate-secret" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
  -d '{
  "overlap_hours": 24
}'
PHP
$client = new \GuzzleHttp\Client([
    'base_uri' => 'https://app.klantly.com/api/v1/',
    'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);

$response = $client->request('POST', 'webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/rotate-secret', [
    'headers' => [
        'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
    ],
    'json' => [
        'overlap_hours' => 24,
    ],
]);

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/rotate-secret', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
  },
  body: JSON.stringify({
  "overlap_hours": 24
}),
});

const { data } = await response.json();
Python
import os

import requests

response = requests.post(
    "https://app.klantly.com/api/v1/webhook-endpoints/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/rotate-secret",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
        "Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    },
    json={
        "overlap_hours": 24
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "webhook_endpoint",
    "id": "01j7zr8m2k4n6p8r0t2v4w6y8a",
    "url": "https://example.com/webhooks/klantly",
    "description": "Boekhouding",
    "events": [
      "customer.created",
      "deal.won"
    ],
    "status": "active",
    "disabled_reason": null,
    "disabled_at": null,
    "previous_secret_expires_at": null,
    "last_success_at": "2026-09-14T10:15:02Z",
    "last_failure_at": null,
    "created_at": "2026-09-14T10:15:00Z",
    "updated_at": "2026-09-14T10:15:00Z",
    "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
  }
}

Possible errors

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

The object

All fields are always present; a field without a value is null.

FieldTypeDescription
object string Always "webhook_endpoint".
id string Id of the endpoint.
url string (uri) The https URL Klantly sends events to. No internal addresses, and only port 443, 80 or 8443.
description string Your own description, for example what the endpoint is for. can be empty (null)
events array<string> The event types this endpoint receives, or ["*"] for every event the key or user can read.
status string active or disabled. one of: active, disabled
disabled_reason string Why the endpoint is off: failing (only errors for 5 days) or manual, otherwise null. can be empty (null) · one of: failing, manual
disabled_at string (date-time) When the endpoint was disabled. can be empty (null)
previous_secret_expires_at string (date-time) Until when the previous secret is still used after a rotation, otherwise null. can be empty (null)
last_success_at string (date-time) Last successful delivery. can be empty (null)
last_failure_at string (date-time) Last failed delivery. can be empty (null)
created_at string (date-time) Created at (UTC).
updated_at string (date-time) Last updated at (UTC).