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
-
GET
/quotesList quotes -
GET
/quotes/{quote}Get a quote -
POST
/quotesCreate a quote -
PATCH
/quotes/{quote}Update a quote -
POST
/quotes/{quote}/sendSend a quote -
POST
/quotes/{quote}/acceptAccept a quote -
POST
/quotes/{quote}/rejectReject a quote -
DELETE
/quotes/{quote}Delete a quote
List quotes
/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
| Name | Type | Description |
|---|---|---|
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 "https://app.klantly.com/api/v1/quotes?filter[status]=sent&sort=-updated_at" \
-H "Authorization: Bearer $KLANTLY_API_KEY"$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'];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();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.
{
"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
-
403
insufficient_scope— This API key has no access to this action. -
422
validation_failed— The input is invalid.
Get a quote
/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
| Name | Type | Description |
|---|---|---|
quote required |
string (uuid) | The id (UUID) of the quote. |
Example request
curl "https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
-H "Authorization: Bearer $KLANTLY_API_KEY"$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'];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();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
{
"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
-
403
insufficient_scope— This API key has no access to this action. -
404
not_found— Not found.
Create a quote
/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)
| Field | Type | Description |
|---|---|---|
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 -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"
}
]
}'$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'];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();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
{
"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
-
403
insufficient_scope— This API key has no access to this action. -
422
validation_failed— The input is invalid. -
422
unknown_field— The input contains an unknown field. -
415
unsupported_media_type— This format is not supported. -
413
payload_too_large— The request body is too large. -
403
limit_reached— The subscription limit has been reached. -
422
idempotency_key_reused— This Idempotency-Key was already used for a different request. -
409
idempotency_in_progress— A request with this Idempotency-Key is still in progress.
Update a quote
/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
| Name | Type | Description |
|---|---|---|
quote required |
string (uuid) | The id (UUID) of the quote. |
Body (JSON)
| Field | Type | Description |
|---|---|---|
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 -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)"
}'$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'];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();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
{
"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
-
403
insufficient_scope— This API key has no access to this action. -
422
validation_failed— The input is invalid. -
422
unknown_field— The input contains an unknown field. -
415
unsupported_media_type— This format is not supported. -
413
payload_too_large— The request body is too large. -
404
not_found— Not found. -
412
precondition_failed— The record has been changed in the meantime. -
409
invalid_state_transition— This action is not possible in the current status.
Send a quote
/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
| Name | Type | Description |
|---|---|---|
quote required |
string (uuid) | The id (UUID) of the quote. |
Body (JSON)
| Field | Type | Description |
|---|---|---|
message
optional
|
string | Personal message in the email to the customer. can be empty (null) · at most 5000 characters |
Example request
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."
}'$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'];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();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
{
"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
-
403
insufficient_scope— This API key has no access to this action. -
422
validation_failed— The input is invalid. -
422
unknown_field— The input contains an unknown field. -
415
unsupported_media_type— This format is not supported. -
413
payload_too_large— The request body is too large. -
404
not_found— Not found. -
403
forbidden— Access denied. -
409
invalid_state_transition— This action is not possible in the current status. -
422
idempotency_key_reused— This Idempotency-Key was already used for a different request. -
409
idempotency_in_progress— A request with this Idempotency-Key is still in progress.
Accept a quote
/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
| Name | Type | Description |
|---|---|---|
quote required |
string (uuid) | The id (UUID) of the quote. |
Example request
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"$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'];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();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
{
"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
-
403
insufficient_scope— This API key has no access to this action. -
404
not_found— Not found. -
409
invalid_state_transition— This action is not possible in the current status. -
422
idempotency_key_reused— This Idempotency-Key was already used for a different request. -
409
idempotency_in_progress— A request with this Idempotency-Key is still in progress.
Reject a quote
/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
| Name | Type | Description |
|---|---|---|
quote required |
string (uuid) | The id (UUID) of the quote. |
Example request
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"$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'];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();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
{
"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
-
403
insufficient_scope— This API key has no access to this action. -
404
not_found— Not found. -
409
invalid_state_transition— This action is not possible in the current status. -
422
idempotency_key_reused— This Idempotency-Key was already used for a different request. -
409
idempotency_in_progress— A request with this Idempotency-Key is still in progress.
Delete a quote
/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
| Name | Type | Description |
|---|---|---|
quote required |
string (uuid) | The id (UUID) of the quote. |
Example request
curl -X DELETE "https://app.klantly.com/api/v1/quotes/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
-H "Authorization: Bearer $KLANTLY_API_KEY"$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'];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();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
{
"data": {
"object": "note",
"id": "9d3f7b41-2d6f-4e8c-9b3a-4f5d6e7a8b92",
"deleted": true
}
}Possible errors
-
403
insufficient_scope— This API key has no access to this action. -
404
not_found— Not found. -
409
invalid_state_transition— This action is not possible in the current status.
The object
All fields are always present; a field without a value is null.
| Field | Type | Description |
|---|---|---|
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. |