Voor ontwikkelaars

Publieke API (v1)

Koppel Kozynofferte aan je eigen boekhouding, rapportage of script. Met een API-key lees je je offertes en klanten uit en maak je klanten aan. Als beheerder maak je een key aan in het dashboard, onder Instellingen → Integraties → Publieke API (v1). De technische referentie hieronder is in het Engels, zoals bij de meeste API's gebruikelijk.

API-key aanmaken Vragen over de API? Mail ons op hallo@kozynofferte.nl.

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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Generate 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:

ScopeEndpoint
quotes:readGET /api/v1/quotes and GET /api/v1/quotes/:id
customers:readGET /api/v1/customers
customers:writePOST /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

StatusMeaning
401Missing/malformed Authorization header, or the key doesn't match any active key
404Resource doesn't exist, or belongs to a different tenant
422Request body failed validation (POST endpoints) — see issues in the response body
429Rate 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, default 1
  • ?limit= — items per page, default 25, max 100

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):

FieldTypeNotes
namestring, 2–160 charsrequired
emailstringvalid email or omitted
phonestring, ≤40 chars
companystring, ≤160 chars
kvkstring, ≤40 chars
addressstring, ≤200 chars
postcodestring, ≤20 chars
citystring, ≤100 chars
notesstring, ≤4000 chars
tagsstring[], ≤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)