Klantly Developers

API reference

Quotes

Quotes with their lines: draft them, update them while they are still drafts, email them to the customer and record the outcome.

Endpoints

List quotes

GET /api/v1/quotes

A list of quotes, newest first, with their lines. Filter by status, customer or change date. Use filter[status]=sent together with filter[updated_since] to pick up what has been sent since your last sync.

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

Query parameters

NameTypeDescription
limit integer Number of results per page. from 1 to 100 · default: 50
cursor string The next_cursor or prev_cursor from meta of the previous response.
sort string Sort by created_at or updated_at; a leading minus sign sorts descending. one of: -created_at, created_at, -updated_at, updated_at · default: -created_at
filter[status] string Only quotes with this status, for example sent or accepted. one of: draft, pending_review, sent, viewed, accepted, rejected, expired, completed
filter[customer_id] string (uuid) Only what belongs to this customer.
filter[updated_since] string (date-time) Only what changed since this moment: ISO 8601 with a time zone, for example 2026-09-14T10:15:00Z. Useful for synchronising.

Example request

cURL
curl "https://app.klantly.com/api/v1/quotes?filter[status]=sent&sort=-updated_at" \
  -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', [
    'query' => [
        'filter[status]' => 'sent',
        'sort' => '-updated_at',
    ],
]);

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/quotes?filter[status]=sent&sort=-updated_at', {
  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",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
    params={
        "filter[status]": "sent",
        "sort": "-updated_at"
    },
)
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": "quote",
      "id": "9d3f8449-afe1-4a6e-9dbc-c1dfe0f1a2ba",
      "number": "OFF-00042",
      "status": "sent",
      "title": "Veranda 400 × 300",
      "description": null,
      "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
      "customer": {
        "type": "business",
        "name": "De Vries Bouw",
        "email": "jan@example.com",
        "phone": "+31 6 12345678",
        "address": "Dorpsstraat 1",
        "postal_code": "3511 AB",
        "city": "Utrecht",
        "country": "NL",
        "company_name": "De Vries Bouw",
        "vat_number": null,
        "coc_number": null
      },
      "deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
      "pipeline_stage_id": "stg_0q8v4n61h9x0zc",
      "language": "nl",
      "currency": "EUR",
      "quote_date": "2026-09-16",
      "valid_until": "2026-09-30",
      "intro_text": null,
      "outro_text": null,
      "terms_conditions": null,
      "notes": null,
      "hide_line_amounts": false,
      "discount_percentage": "0.00",
      "discount_amount": "0.00",
      "discount_description": null,
      "subtotal": "2450.00",
      "tax_amount": "514.50",
      "total": "2964.50",
      "sent_at": null,
      "viewed_at": null,
      "accepted_at": null,
      "rejected_at": null,
      "invoice_id": null,
      "items": [
        {
          "object": "quote_item",
          "id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cc",
          "type": "product",
          "name": "Veranda",
          "description": null,
          "sku": null,
          "quantity": "1.00",
          "unit": "stuk",
          "unit_price": "2450.00",
          "unit_price_incl": null,
          "discount_percentage": "0.00",
          "discount_amount": "0.00",
          "discount_description": null,
          "line_total": "2450.00",
          "line_total_incl": null,
          "is_taxable": true,
          "tax_rate": "21.00"
        }
      ],
      "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.

Get a quote

GET /api/v1/quotes/{quote}

One quote by id, with its lines and amounts. The response carries an ETag you can send back in If-Match when updating.

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" \
  -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');

$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', {
  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",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "quote",
    "id": "9d3f8449-afe1-4a6e-9dbc-c1dfe0f1a2ba",
    "number": "OFF-00042",
    "status": "sent",
    "title": "Veranda 400 × 300",
    "description": null,
    "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    "customer": {
      "type": "business",
      "name": "De Vries Bouw",
      "email": "jan@example.com",
      "phone": "+31 6 12345678",
      "address": "Dorpsstraat 1",
      "postal_code": "3511 AB",
      "city": "Utrecht",
      "country": "NL",
      "company_name": "De Vries Bouw",
      "vat_number": null,
      "coc_number": null
    },
    "deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
    "pipeline_stage_id": "stg_0q8v4n61h9x0zc",
    "language": "nl",
    "currency": "EUR",
    "quote_date": "2026-09-16",
    "valid_until": "2026-09-30",
    "intro_text": null,
    "outro_text": null,
    "terms_conditions": null,
    "notes": null,
    "hide_line_amounts": false,
    "discount_percentage": "0.00",
    "discount_amount": "0.00",
    "discount_description": null,
    "subtotal": "2450.00",
    "tax_amount": "514.50",
    "total": "2964.50",
    "sent_at": null,
    "viewed_at": null,
    "accepted_at": null,
    "rejected_at": null,
    "invoice_id": null,
    "items": [
      {
        "object": "quote_item",
        "id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cc",
        "type": "product",
        "name": "Veranda",
        "description": null,
        "sku": null,
        "quantity": "1.00",
        "unit": "stuk",
        "unit_price": "2450.00",
        "unit_price_incl": null,
        "discount_percentage": "0.00",
        "discount_amount": "0.00",
        "discount_description": null,
        "line_total": "2450.00",
        "line_total_incl": null,
        "is_taxable": true,
        "tax_rate": "21.00"
      }
    ],
    "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 quote

POST /api/v1/quotes

Creates a quote for a customer, always as a draft. Klantly assigns the number and calculates the totals; name, address and contact details come from the customer. Send it afterwards with Send a quote.

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

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

Body (JSON)

FieldTypeDescription
customer_id required string (uuid) The customer. Required when creating; name, address and contact details come from the customer.
title required string Title of the quote. Required when creating. at most 255 characters
items required array The lines (up to 200). When sending: a list with name (required) per line and optionally type, description, sku, quantity, unit, unit_price, unit_price_incl, discount_percentage, discount_amount, discount_description, tax_rate and is_taxable. They replace all existing lines.
description optional string Short description; shown above the lines. can be empty (null) · at most 20000 characters
language optional string Language of the quote: nl, en, de or fr. one of: nl, en, de, fr
quote_date optional string (date) Quote date (YYYY-MM-DD).
valid_until optional string (date) Valid through (YYYY-MM-DD). Leave it out and the company's default term applies.
intro_text optional string Introduction above the lines. can be empty (null) · at most 20000 characters
outro_text optional string Closing text below the lines. can be empty (null) · at most 20000 characters
terms_conditions optional string Terms shown on the quote. can be empty (null) · at most 20000 characters
notes optional string Notes for the customer. can be empty (null) · at most 20000 characters
hide_line_amounts optional boolean Hides the per-line amounts on the quote; the customer only sees the total.
discount_percentage optional number Discount on the total, as a percentage. from 0 to 100
discount_amount optional number Discount on the total, as an amount. from 0 to 9999999
discount_description optional string Why the discount was given; shown on the quote. can be empty (null) · at most 500 characters

Example request

cURL
curl -X POST "https://app.klantly.com/api/v1/quotes" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
  -d '{
  "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
  "title": "Veranda 400 × 300",
  "items": [
    {
      "name": "Veranda",
      "quantity": 1,
      "unit_price": "2450.00",
      "tax_rate": "21.00"
    }
  ]
}'
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', [
    'headers' => [
        'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
    ],
    'json' => [
        'customer_id' => '9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70',
        'title' => 'Veranda 400 × 300',
        'items' => [
            0 => [
                'name' => 'Veranda',
                'quantity' => 1,
                'unit_price' => '2450.00',
                'tax_rate' => '21.00',
            ],
        ],
    ],
]);

$data = json_decode((string) $response->getBody(), true)['data'];
JavaScript
const response = await fetch('https://app.klantly.com/api/v1/quotes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
  },
  body: JSON.stringify({
  "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
  "title": "Veranda 400 × 300",
  "items": [
    {
      "name": "Veranda",
      "quantity": 1,
      "unit_price": "2450.00",
      "tax_rate": "21.00"
    }
  ]
}),
});

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

import requests

response = requests.post(
    "https://app.klantly.com/api/v1/quotes",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
        "Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    },
    json={
        "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
        "title": "Veranda 400 × 300",
        "items": [
            {
                "name": "Veranda",
                "quantity": 1,
                "unit_price": "2450.00",
                "tax_rate": "21.00"
            }
        ]
    },
)
data = response.json()["data"]

Response 201

Example response
{
  "data": {
    "object": "quote",
    "id": "9d3f8449-afe1-4a6e-9dbc-c1dfe0f1a2ba",
    "number": "OFF-00042",
    "status": "sent",
    "title": "Veranda 400 × 300",
    "description": null,
    "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    "customer": {
      "type": "business",
      "name": "De Vries Bouw",
      "email": "jan@example.com",
      "phone": "+31 6 12345678",
      "address": "Dorpsstraat 1",
      "postal_code": "3511 AB",
      "city": "Utrecht",
      "country": "NL",
      "company_name": "De Vries Bouw",
      "vat_number": null,
      "coc_number": null
    },
    "deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
    "pipeline_stage_id": "stg_0q8v4n61h9x0zc",
    "language": "nl",
    "currency": "EUR",
    "quote_date": "2026-09-16",
    "valid_until": "2026-09-30",
    "intro_text": null,
    "outro_text": null,
    "terms_conditions": null,
    "notes": null,
    "hide_line_amounts": false,
    "discount_percentage": "0.00",
    "discount_amount": "0.00",
    "discount_description": null,
    "subtotal": "2450.00",
    "tax_amount": "514.50",
    "total": "2964.50",
    "sent_at": null,
    "viewed_at": null,
    "accepted_at": null,
    "rejected_at": null,
    "invoice_id": null,
    "items": [
      {
        "object": "quote_item",
        "id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cc",
        "type": "product",
        "name": "Veranda",
        "description": null,
        "sku": null,
        "quantity": "1.00",
        "unit": "stuk",
        "unit_price": "2450.00",
        "unit_price_incl": null,
        "discount_percentage": "0.00",
        "discount_amount": "0.00",
        "discount_description": null,
        "line_total": "2450.00",
        "line_total_incl": null,
        "is_taxable": true,
        "tax_rate": "21.00"
      }
    ],
    "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.

Update a quote

PATCH /api/v1/quotes/{quote}

Changes only the fields you send. items replaces all lines as a whole, and those lines get new ids. Only a draft can still change: once the quote has been sent the customer has seen it, and you get a 409.

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

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

Path parameters

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

Body (JSON)

FieldTypeDescription
title optional string Title of the quote. Required when creating. at most 255 characters
description optional string Short description; shown above the lines. can be empty (null) · at most 20000 characters
language optional string Language of the quote: nl, en, de or fr. one of: nl, en, de, fr
quote_date optional string (date) Quote date (YYYY-MM-DD).
valid_until optional string (date) Valid through (YYYY-MM-DD). Leave it out and the company's default term applies.
intro_text optional string Introduction above the lines. can be empty (null) · at most 20000 characters
outro_text optional string Closing text below the lines. can be empty (null) · at most 20000 characters
terms_conditions optional string Terms shown on the quote. can be empty (null) · at most 20000 characters
notes optional string Notes for the customer. can be empty (null) · at most 20000 characters
hide_line_amounts optional boolean Hides the per-line amounts on the quote; the customer only sees the total.
discount_percentage optional number Discount on the total, as a percentage. from 0 to 100
discount_amount optional number Discount on the total, as an amount. from 0 to 9999999
discount_description optional string Why the discount was given; shown on the quote. can be empty (null) · at most 500 characters
items optional array The lines (up to 200). When sending: a list with name (required) per line and optionally type, description, sku, quantity, unit, unit_price, unit_price_incl, discount_percentage, discount_amount, discount_description, tax_rate and is_taxable. They replace all existing lines.

Example request

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

$response = $client->request('PATCH', 'quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', [
    'json' => [
        'title' => 'Veranda 400 × 300 (herzien)',
    ],
]);

$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', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "title": "Veranda 400 × 300 (herzien)"
}),
});

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

import requests

response = requests.patch(
    "https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
    },
    json={
        "title": "Veranda 400 × 300 (herzien)"
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "quote",
    "id": "9d3f8449-afe1-4a6e-9dbc-c1dfe0f1a2ba",
    "number": "OFF-00042",
    "status": "sent",
    "title": "Veranda 400 × 300",
    "description": null,
    "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    "customer": {
      "type": "business",
      "name": "De Vries Bouw",
      "email": "jan@example.com",
      "phone": "+31 6 12345678",
      "address": "Dorpsstraat 1",
      "postal_code": "3511 AB",
      "city": "Utrecht",
      "country": "NL",
      "company_name": "De Vries Bouw",
      "vat_number": null,
      "coc_number": null
    },
    "deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
    "pipeline_stage_id": "stg_0q8v4n61h9x0zc",
    "language": "nl",
    "currency": "EUR",
    "quote_date": "2026-09-16",
    "valid_until": "2026-09-30",
    "intro_text": null,
    "outro_text": null,
    "terms_conditions": null,
    "notes": null,
    "hide_line_amounts": false,
    "discount_percentage": "0.00",
    "discount_amount": "0.00",
    "discount_description": null,
    "subtotal": "2450.00",
    "tax_amount": "514.50",
    "total": "2964.50",
    "sent_at": null,
    "viewed_at": null,
    "accepted_at": null,
    "rejected_at": null,
    "invoice_id": null,
    "items": [
      {
        "object": "quote_item",
        "id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cc",
        "type": "product",
        "name": "Veranda",
        "description": null,
        "sku": null,
        "quantity": "1.00",
        "unit": "stuk",
        "unit_price": "2450.00",
        "unit_price_incl": null,
        "discount_percentage": "0.00",
        "discount_amount": "0.00",
        "discount_description": null,
        "line_total": "2450.00",
        "line_total_incl": null,
        "is_taxable": true,
        "tax_rate": "21.00"
      }
    ],
    "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.

Send a quote

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

Emails the quote to the customer using the company's notification template, sets the status to sent and records the time. Your own message appears in the email. Sending again after a rejection puts the quote back to sent.

Scope
quotes.send — Email quotes to customers
Required feature
quotes

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

Path parameters

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

Body (JSON)

FieldTypeDescription
message optional string Personal message in the email to the customer. can be empty (null) · at most 5000 characters

Example request

cURL
curl -X POST "https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/send" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
  -d '{
  "message": "Bijgaand de offerte die we vanmorgen bespraken."
}'
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/send', [
    'headers' => [
        'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
    ],
    'json' => [
        'message' => 'Bijgaand de offerte die we vanmorgen bespraken.',
    ],
]);

$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/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
  },
  body: JSON.stringify({
  "message": "Bijgaand de offerte die we vanmorgen bespraken."
}),
});

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/send",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
        "Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    },
    json={
        "message": "Bijgaand de offerte die we vanmorgen bespraken."
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "quote",
    "id": "9d3f8449-afe1-4a6e-9dbc-c1dfe0f1a2ba",
    "number": "OFF-00042",
    "status": "sent",
    "title": "Veranda 400 × 300",
    "description": null,
    "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    "customer": {
      "type": "business",
      "name": "De Vries Bouw",
      "email": "jan@example.com",
      "phone": "+31 6 12345678",
      "address": "Dorpsstraat 1",
      "postal_code": "3511 AB",
      "city": "Utrecht",
      "country": "NL",
      "company_name": "De Vries Bouw",
      "vat_number": null,
      "coc_number": null
    },
    "deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
    "pipeline_stage_id": "stg_0q8v4n61h9x0zc",
    "language": "nl",
    "currency": "EUR",
    "quote_date": "2026-09-16",
    "valid_until": "2026-09-30",
    "intro_text": null,
    "outro_text": null,
    "terms_conditions": null,
    "notes": null,
    "hide_line_amounts": false,
    "discount_percentage": "0.00",
    "discount_amount": "0.00",
    "discount_description": null,
    "subtotal": "2450.00",
    "tax_amount": "514.50",
    "total": "2964.50",
    "sent_at": null,
    "viewed_at": null,
    "accepted_at": null,
    "rejected_at": null,
    "invoice_id": null,
    "items": [
      {
        "object": "quote_item",
        "id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cc",
        "type": "product",
        "name": "Veranda",
        "description": null,
        "sku": null,
        "quantity": "1.00",
        "unit": "stuk",
        "unit_price": "2450.00",
        "unit_price_incl": null,
        "discount_percentage": "0.00",
        "discount_amount": "0.00",
        "discount_description": null,
        "line_total": "2450.00",
        "line_total_incl": null,
        "is_taxable": true,
        "tax_rate": "21.00"
      }
    ],
    "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.

Accept a quote

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

Records that the customer agreed, for instance after an approval outside Klantly. An accepted quote can no longer be rejected.

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

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

Path parameters

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

Example request

cURL
curl -X POST "https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/accept" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f"
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/accept', [
    'headers' => [
        'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
    ],
]);

$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/accept', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
    'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
  },
});

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/accept",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
        "Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "quote",
    "id": "9d3f8449-afe1-4a6e-9dbc-c1dfe0f1a2ba",
    "number": "OFF-00042",
    "status": "sent",
    "title": "Veranda 400 × 300",
    "description": null,
    "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    "customer": {
      "type": "business",
      "name": "De Vries Bouw",
      "email": "jan@example.com",
      "phone": "+31 6 12345678",
      "address": "Dorpsstraat 1",
      "postal_code": "3511 AB",
      "city": "Utrecht",
      "country": "NL",
      "company_name": "De Vries Bouw",
      "vat_number": null,
      "coc_number": null
    },
    "deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
    "pipeline_stage_id": "stg_0q8v4n61h9x0zc",
    "language": "nl",
    "currency": "EUR",
    "quote_date": "2026-09-16",
    "valid_until": "2026-09-30",
    "intro_text": null,
    "outro_text": null,
    "terms_conditions": null,
    "notes": null,
    "hide_line_amounts": false,
    "discount_percentage": "0.00",
    "discount_amount": "0.00",
    "discount_description": null,
    "subtotal": "2450.00",
    "tax_amount": "514.50",
    "total": "2964.50",
    "sent_at": null,
    "viewed_at": null,
    "accepted_at": null,
    "rejected_at": null,
    "invoice_id": null,
    "items": [
      {
        "object": "quote_item",
        "id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cc",
        "type": "product",
        "name": "Veranda",
        "description": null,
        "sku": null,
        "quantity": "1.00",
        "unit": "stuk",
        "unit_price": "2450.00",
        "unit_price_incl": null,
        "discount_percentage": "0.00",
        "discount_amount": "0.00",
        "discount_description": null,
        "line_total": "2450.00",
        "line_total_incl": null,
        "is_taxable": true,
        "tax_rate": "21.00"
      }
    ],
    "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.

Reject a quote

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

Records that the customer declined. You can send a revised quote afterwards, which puts it back to sent.

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

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

Path parameters

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

Example request

cURL
curl -X POST "https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/reject" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f"
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/reject', [
    'headers' => [
        'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
    ],
]);

$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/reject', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
    'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
  },
});

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/reject",
    headers={
        "Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
        "Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    },
)
data = response.json()["data"]

Response 200

Example response
{
  "data": {
    "object": "quote",
    "id": "9d3f8449-afe1-4a6e-9dbc-c1dfe0f1a2ba",
    "number": "OFF-00042",
    "status": "sent",
    "title": "Veranda 400 × 300",
    "description": null,
    "customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
    "customer": {
      "type": "business",
      "name": "De Vries Bouw",
      "email": "jan@example.com",
      "phone": "+31 6 12345678",
      "address": "Dorpsstraat 1",
      "postal_code": "3511 AB",
      "city": "Utrecht",
      "country": "NL",
      "company_name": "De Vries Bouw",
      "vat_number": null,
      "coc_number": null
    },
    "deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
    "pipeline_stage_id": "stg_0q8v4n61h9x0zc",
    "language": "nl",
    "currency": "EUR",
    "quote_date": "2026-09-16",
    "valid_until": "2026-09-30",
    "intro_text": null,
    "outro_text": null,
    "terms_conditions": null,
    "notes": null,
    "hide_line_amounts": false,
    "discount_percentage": "0.00",
    "discount_amount": "0.00",
    "discount_description": null,
    "subtotal": "2450.00",
    "tax_amount": "514.50",
    "total": "2964.50",
    "sent_at": null,
    "viewed_at": null,
    "accepted_at": null,
    "rejected_at": null,
    "invoice_id": null,
    "items": [
      {
        "object": "quote_item",
        "id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cc",
        "type": "product",
        "name": "Veranda",
        "description": null,
        "sku": null,
        "quantity": "1.00",
        "unit": "stuk",
        "unit_price": "2450.00",
        "unit_price_incl": null,
        "discount_percentage": "0.00",
        "discount_amount": "0.00",
        "discount_description": null,
        "line_total": "2450.00",
        "line_total_incl": null,
        "is_taxable": true,
        "tax_rate": "21.00"
      }
    ],
    "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 quote

DELETE /api/v1/quotes/{quote}

Deletes the quote. Only a draft can go: a quote that has been sent stays.

Scope
quotes.delete — Delete quotes
Required feature
quotes

Path parameters

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

Example request

cURL
curl -X DELETE "https://app.klantly.com/api/v1/quotes/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');

$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', {
  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",
    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 "quote".
id string (uuid) Unique id (UUID).
number string Quote number, for example OFF-00042; Klantly assigns it.
status string draft, pending_review, sent, viewed, accepted, rejected, expired or completed. one of: draft, pending_review, sent, viewed, accepted, rejected, expired, completed
title string Title of the quote. Required when creating. can be empty (null)
description string Short description; shown above the lines. can be empty (null)
customer_id string (uuid) The customer. Required when creating; name, address and contact details come from the customer. can be empty (null)
customer object The customer details on the quote, as they were when it was created.
customer.type string individual or business. can be empty (null)
customer.name string Name; for a business the company name. can be empty (null)
customer.email string Email address. can be empty (null)
customer.phone string Phone number. can be empty (null)
customer.address string Street and house number. can be empty (null)
customer.postal_code string Postal code. can be empty (null)
customer.city string City. can be empty (null)
customer.country string Country. can be empty (null)
customer.company_name string Company name. can be empty (null)
customer.vat_number string VAT number. can be empty (null)
customer.coc_number string Chamber of commerce number. can be empty (null)
deal_id string (uuid) The deal on the pipeline board this quote belongs to, or null. can be empty (null)
pipeline_stage_id string The stage the quote sits in, or null. can be empty (null)
language string Language of the quote: nl, en, de or fr. one of: nl, en, de, fr
currency string Always "EUR".
quote_date string (date) Quote date (YYYY-MM-DD). can be empty (null)
valid_until string (date) Valid through (YYYY-MM-DD). Leave it out and the company's default term applies. can be empty (null)
intro_text string Introduction above the lines. can be empty (null)
outro_text string Closing text below the lines. can be empty (null)
terms_conditions string Terms shown on the quote. can be empty (null)
notes string Notes for the customer. can be empty (null)
hide_line_amounts boolean Hides the per-line amounts on the quote; the customer only sees the total.
discount_percentage string Discount on the total, as a percentage.
discount_amount string Discount on the total, as an amount.
discount_description string Why the discount was given; shown on the quote. can be empty (null)
subtotal string Total excluding VAT, as a string with two decimals.
tax_amount string VAT amount.
total string Total including VAT.
sent_at string (date-time) When the quote was sent to the customer. can be empty (null)
viewed_at string (date-time) When the customer opened it. can be empty (null)
accepted_at string (date-time) When the customer approved it. can be empty (null)
rejected_at string (date-time) When the customer declined it. can be empty (null)
invoice_id string (uuid) The invoice created from this quote, or null. can be empty (null)
items array<object> The lines (up to 200). When sending: a list with name (required) per line and optionally type, description, sku, quantity, unit, unit_price, unit_price_incl, discount_percentage, discount_amount, discount_description, tax_rate and is_taxable. They replace all existing lines.
items.object string Always "quote_item".
items.id string (uuid) Unique id of the line (UUID).
items.type string product, service, discount or fee. one of: product, service, discount, fee
items.name string Name of the line.
items.description string Description below the line. can be empty (null)
items.sku string Article number. can be empty (null)
items.quantity string Quantity.
items.unit string Unit, for example piece or hour. can be empty (null)
items.unit_price string Price per unit, excluding VAT.
items.unit_price_incl string Unit price including VAT, as entered. When set, the line is calculated from this amount (unit_price and line_total are derived from it), so the total matches the entered price to the cent. null = the line is calculated from unit_price. can be empty (null)
items.discount_percentage string Discount on this line, as a percentage.
items.discount_amount string Discount on this line, as an amount.
items.discount_description string Why the discount on this line was given. can be empty (null)
items.line_total string Line total excluding VAT, after discount.
items.line_total_incl string Line total including VAT, after discount. Only set when the line is calculated from unit_price_incl; otherwise null. can be empty (null)
items.is_taxable boolean Whether this line counts towards VAT.
items.tax_rate string VAT rate of this line, as a percentage.
created_at string (date-time) When the quote was created.
updated_at string (date-time) When the quote was last changed.