Klantly Developers

API reference

Attachments

Files on a quote and photos on a work order: list, upload and delete. Downloads go through a signed link that is valid for fifteen minutes and needs no API key.

Endpoints

List attachments

GET /api/v1/quotes/{quote}/attachments

All attachments on this quote, oldest first, each with a signed download link.

Scope
quotes.read — Read quotes, with their lines, amounts and the customer details on them
Required feature
quotes

Path parameters

NameTypeDescription
quote required string (uuid) The id (UUID) of the quote.

Example request

cURL
curl "https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments" \
  -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', 'quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments', {
  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/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments",
    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": "attachment",
      "id": "9d3f9e56-af10-4123-b345-e6f7a8b9c0d5",
      "quote_id": "9d3f8449-afe1-4a6e-9dbc-c1dfe0f1a2ba",
      "name": "technische-tekening.pdf",
      "mime_type": "application/pdf",
      "size": 248000,
      "description": "Tekening van de dakopbouw",
      "visible_to_customer": true,
      "download_url": "https://app.klantly.com/api/files/quote-attachment/9d3f9e56-af10-4123-b345-e6f7a8b9c0d5?expires=1789000000&signature=8f1c…",
      "created_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.

Upload attachment

POST /api/v1/quotes/{quote}/attachments

Adds a file to the quote. Send it as multipart/form-data in the field file — together with the work-order photo this is the only endpoint that does not expect JSON. Allowed are pdf, jpg, jpeg, png, gif, webp, doc, docx, xls and xlsx, up to 10 MB per file and 20 attachments per quote; beyond that you get 403 limit_reached. The file type is checked against the contents, not the name.

Scope
quotes.write — Create and update quotes (drafts only), and accept or reject them on the customer's behalf
Required feature
quotes

Path parameters

NameTypeDescription
quote required string (uuid) The id (UUID) of the quote.

Body (JSON)

FieldTypeDescription
file required string (binary) The file itself, as multipart/form-data. Up to 10 MB; allowed are pdf, jpg, jpeg, png, gif, webp, doc, docx, xls and xlsx.
description optional string Your own description of the attachment. can be empty (null) · at most 255 characters
visible_to_customer optional boolean True if the customer receives this attachment with the quote.

Example request

cURL
curl -X POST "https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments" \
  -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', 'quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments', {
  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/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
)
data = response.json()["data"]

Response 201

Example response
{
  "data": {
    "object": "attachment",
    "id": "9d3f9e56-af10-4123-b345-e6f7a8b9c0d5",
    "quote_id": "9d3f8449-afe1-4a6e-9dbc-c1dfe0f1a2ba",
    "name": "technische-tekening.pdf",
    "mime_type": "application/pdf",
    "size": 248000,
    "description": "Tekening van de dakopbouw",
    "visible_to_customer": true,
    "download_url": "https://app.klantly.com/api/files/quote-attachment/9d3f9e56-af10-4123-b345-e6f7a8b9c0d5?expires=1789000000&signature=8f1c…",
    "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.

Delete attachment

DELETE /api/v1/quotes/{quote}/attachments/{attachment}

Deletes the attachment and the file itself. This cannot be undone.

Scope
quotes.write — Create and update quotes (drafts only), and accept or reject them on the customer's behalf
Required feature
quotes

Path parameters

NameTypeDescription
quote required string (uuid) The id (UUID) of the quote.
attachment required string (uuid) The id (UUID) of the attachment.

Example request

cURL
curl -X DELETE "https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments/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', 'quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments/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/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/attachments/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.

List work-order photos

GET /api/v1/work-orders/{work_order}/photos

All photos on this work order, in the order they appear on it, each with a signed download link.

Scope
work_orders.read — Read work orders, with the customer's name, address and contact details
Required feature
work_orders

Path parameters

NameTypeDescription
work_order required string (uuid) The id (UUID) of the work order.

Example request

cURL
curl "https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos" \
  -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', 'work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos', {
  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/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos",
    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": "work_order_photo",
      "id": "9d3f9f67-b021-4234-c456-f7a8b9c0d1e6",
      "work_order_id": "9d3f8328-9ed0-4f5d-8cab-b0cedfe0f1a9",
      "kind": "after",
      "caption": "Dakgoot na montage",
      "download_url": "https://app.klantly.com/api/files/quote-attachment/9d3f9e56-af10-4123-b345-e6f7a8b9c0d5?expires=1789000000&signature=8f1c…",
      "created_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.

Upload work-order photo

POST /api/v1/work-orders/{work_order}/photos

Adds a photo to the work order; it also appears in the PDF. Send it as multipart/form-data in the field file. Images only (jpg, jpeg, png, gif, webp), up to 10 MB per photo and 30 photos per work order. Use kind to say whether the photo is from before or after the job.

Scope
work_orders.write — Create and update work orders, and change their status (completing can send a review request)
Required feature
work_orders

Path parameters

NameTypeDescription
work_order required string (uuid) The id (UUID) of the work order.

Body (JSON)

FieldTypeDescription
file required string (binary) The photo itself, as multipart/form-data. Up to 10 MB; allowed are jpg, jpeg, png, gif and webp.
kind optional string before (before the job), after (afterwards) or other. one of: before, after, other
caption optional string Caption for the photo. can be empty (null) · at most 255 characters

Example request

cURL
curl -X POST "https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos" \
  -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', 'work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos', {
  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/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
)
data = response.json()["data"]

Response 201

Example response
{
  "data": {
    "object": "work_order_photo",
    "id": "9d3f9f67-b021-4234-c456-f7a8b9c0d1e6",
    "work_order_id": "9d3f8328-9ed0-4f5d-8cab-b0cedfe0f1a9",
    "kind": "after",
    "caption": "Dakgoot na montage",
    "download_url": "https://app.klantly.com/api/files/quote-attachment/9d3f9e56-af10-4123-b345-e6f7a8b9c0d5?expires=1789000000&signature=8f1c…",
    "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.

Delete work-order photo

DELETE /api/v1/work-orders/{work_order}/photos/{photo}

Deletes the photo and the file itself. This cannot be undone.

Scope
work_orders.write — Create and update work orders, and change their status (completing can send a review request)
Required feature
work_orders

Path parameters

NameTypeDescription
work_order required string (uuid) The id (UUID) of the work order.
photo required string (uuid) The id (UUID) of the photo.

Example request

cURL
curl -X DELETE "https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos/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', 'work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70');

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos/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/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/photos/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.

The object

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

FieldTypeDescription
object string Always "attachment".
id string (uuid) Id of the attachment.
quote_id string (uuid) The quote this attachment belongs to.
name string The file name as it was uploaded. can be empty (null)
mime_type string The file type, such as application/pdf. can be empty (null)
size integer The size in bytes. can be empty (null)
description string Your own description of the attachment. can be empty (null)
visible_to_customer boolean True if the customer receives this attachment with the quote.
download_url string (uri) A signed link to the file, valid for fifteen minutes and openable without an API key. Request it again once it has expired.
created_at string (date-time) When the attachment was added (UTC).