Scope: v1 is intentionally small and read-mostly (one write endpoint: creating a customer). It exists so a tenant can pull their own quotes and customers into an external system (accounting, BI, a script). It is not a general-purpose partner platform yet — more resources and write-scopes may be added later, but treat this document as the full surface today, not a preview of a bigger promise.
Authentication
Every request needs an API key in the Authorization header:
Authorization: Bearer kz_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGenerate a key from the dashboard: Instellingen → Integraties → Publieke API (v1) (/dashboard/settings/api-keys, tenant ADMIN only). The full key is shown exactly once, right after creation — copy it immediately, it cannot be retrieved again. If you lose it, revoke it and create a new one.
A key can be revoked at any time from the same page. Revoking is immediate and permanent (soft-revoked, not deleted — the key stops working right away).
Every key is scoped to the tenant that created it. There is no way to read or write another tenant's data with a valid key — every query is filtered by the tenant the key belongs to, and any request for a resource id that belongs to a different tenant (or doesn't exist) returns a plain 404, not a 403 — the API never confirms whether an id exists outside your tenant.
Scopes
Scopes are enforced per endpoint. New keys receive these defaults:
| Scope | Endpoint |
|---|---|
quotes:read | GET /api/v1/quotes and GET /api/v1/quotes/:id |
customers:read | GET /api/v1/customers |
customers:write | POST /api/v1/customers |
Keys created before customers:write was introduced are upgraded by the database migration. A key without the required scope receives 401.
Rate limit
60 requests per minute, per API key (not per IP — this is a server-to-server rate limit for legitimate authenticated clients). Going over it returns 429 with a Retry-After header (seconds) and these response headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (unix seconds).
Errors
| Status | Meaning |
|---|---|
401 | Missing/malformed Authorization header, or the key doesn't match any active key |
404 | Resource doesn't exist, or belongs to a different tenant |
422 | Request body failed validation (POST endpoints) — see issues in the response body |
429 | Rate limit exceeded |
Error responses are { "error": "..." }, optionally with issues (zod's flatten() output) on 422.
Pagination
List endpoints use simple page-based pagination via query params:
?page=— 1-indexed, default1?limit=— items per page, default25, max100
Response shape:
{
"data": [ /* ... */ ],
"page": 1,
"limit": 25,
"total": 142,
"totalPages": 6
}Endpoints
GET /api/v1/quotes
List the authenticated tenant's quotes, newest first.
curl https://kozynofferte.nl/api/v1/quotes?limit=10 \
-H "Authorization: Bearer kz_live_..."{
"data": [
{
"id": "clx...",
"number": "OFF-2026-00042",
"status": "SENT",
"total": 8450.5,
"createdAt": "2026-08-01T09:12:00.000Z",
"customerName": "Jansen Bouw B.V."
}
],
"page": 1,
"limit": 10,
"total": 37,
"totalPages": 4
}status is one of: DRAFT, SENT, VIEWED, SIGNED_SALES, MEASURED, SIGNED_FINAL, IN_PRODUCTION, DELIVERED, CANCELLED, EXPIRED.
GET /api/v1/quotes/:id
Single quote, with line items.
curl https://kozynofferte.nl/api/v1/quotes/clx... \
-H "Authorization: Bearer kz_live_..."{
"data": {
"id": "clx...",
"number": "OFF-2026-00042",
"status": "SENT",
"total": 8450.5,
"subtotal": 6983.06,
"btwAmount": 1467.44,
"validUntil": "2026-09-01T00:00:00.000Z",
"createdAt": "2026-08-01T09:12:00.000Z",
"updatedAt": "2026-08-01T09:12:00.000Z",
"customer": { "name": "Jansen Bouw B.V.", "email": "info@jansenbouw.nl" },
"lines": [
{
"id": "cly...",
"description": "Draaikiepraam 1200x1400mm, wit, HR++",
"qty": 3,
"unitPrice": 645.5,
"total": 1936.5,
"isOptional": false,
"optionChosen": false
}
]
}
}Lines with isOptional: true are options offered with their own price. They are not part of subtotal/total unless the customer chose them when signing (optionChosen: true); after that, subtotal, btwAmount and total are the agreed amounts including the chosen options.
A quote belonging to another tenant, or an unknown id, returns 404.
GET /api/v1/customers
List the authenticated tenant's customers, most recently updated first.
curl https://kozynofferte.nl/api/v1/customers?page=1&limit=25 \
-H "Authorization: Bearer kz_live_..."{
"data": [
{
"id": "clz...",
"name": "Jansen Bouw B.V.",
"email": "info@jansenbouw.nl",
"phone": "010-1234567",
"company": "Jansen Bouw B.V.",
"city": "Rotterdam",
"createdAt": "2026-03-14T11:02:00.000Z",
"updatedAt": "2026-08-01T09:12:00.000Z"
}
],
"page": 1,
"limit": 25,
"total": 12,
"totalPages": 1
}POST /api/v1/customers
Create a customer under the authenticated tenant.
curl -X POST https://kozynofferte.nl/api/v1/customers \
-H "Authorization: Bearer kz_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "De Vries Installatie",
"email": "contact@devriesinstallatie.nl",
"phone": "06-12345678",
"city": "Schiedam"
}'Body fields (all optional except name):
| Field | Type | Notes |
|---|---|---|
name | string, 2–160 chars | required |
email | string | valid email or omitted |
phone | string, ≤40 chars | |
company | string, ≤160 chars | |
kvk | string, ≤40 chars | |
address | string, ≤200 chars | |
postcode | string, ≤20 chars | |
city | string, ≤100 chars | |
notes | string, ≤4000 chars | |
tags | string[], ≤20 items, each ≤40 chars |
{
"data": {
"id": "clz...",
"name": "De Vries Installatie",
"email": "contact@devriesinstallatie.nl",
"createdAt": "2026-08-30T10:00:00.000Z"
}
}Returns 201 on success, 422 with issues on a validation failure.
Not in scope (yet)
- Write access to quotes, projects, or any resource beyond creating a customer
- Webhooks are a separate feature (see Instellingen → Integraties → Webhooks (REST)) — not part of this API
- Cursor-based pagination (page/limit only, for now)