Authenticating with API keys
Every call to the API uses a credential in the Authorization header. A credential consists of a public prefix and a secret, together in the form kp_<prefix>_<secret>.
Step 1
Create a credential
Go to Settings, Integrations and create an API key with a label and a set of scopes. The full key is shown exactly once, so store it safely right away.
Step 2
Use minimal scopes
Grant a key only the scopes your integration needs. A call without the required scope gets a 403 with code insufficient_scope.
Step 3
Call the API
Send the key as a Bearer token in the Authorization header. Treat it like a password: whoever holds it can act on behalf of your account within its scopes.
curl https://www.keypilot.eu/api/v1/properties?limit=25 \ -H "Authorization: Bearer kp_<prefix>_<secret>"
Scopes
Scopes bound what a credential may do. You combine read and write rights per resource and can rotate or revoke a key at any time.
properties:read
List and retrieve properties.
properties:write
Create and update properties.
contacts:read
List and retrieve contacts (tenants and contractors).
contacts:write
Create and update contacts.
contracts:read
List and retrieve contracts.
contracts:write
Create and update contracts.
tasks:read
List and retrieve tasks.
tasks:write
Create and update tasks.
maintenance:read
List and retrieve maintenance requests.
maintenance:write
Create and update maintenance requests.
webhooks:manage
Manage webhook subscriptions (today via Settings, Integrations).
Pagination
List endpoints use cursor pagination and return results in ascending id order.
Pass limit (1 to 100, default 25) for the page size. As long as nextCursor is not null, fetch the next page with cursor=<nextCursor>. This walks the whole collection without duplicates or gaps.
{ "data": [ { "id": 42, "address": "Grote Markt 1", "...": "…" } ], "pagination": { "limit": 25, "nextCursor": 42 } }
Errors and limits
Every error follows the same envelope: a machine-readable code, a human-readable message and a requestId for support.
{ "error": { "code": "insufficient_scope", "message": "This endpoint requires contacts:write.", "requestId": "9f1c…" } }
unauthorized
No valid credential, or the key was revoked (401).
insufficient_scope
The credential lacks the scope this endpoint requires (403).
invalid_request
The payload or parameters failed validation (400).
not_found
The resource does not exist or does not belong to your account (404).
idempotency_conflict
This Idempotency-Key was already used for a different request (409).
idempotency_in_progress
A request with this Idempotency-Key is still running (409).
rate_limited
Too many requests within the time window (429).
Limit: 900 requests per 15 minutes per credential. The RateLimit headers on every response show what remains.
POST and PATCH calls accept an Idempotency-Key header. Send the same key again and you get the stored response without the mutation running a second time, so retries can never create duplicate records.
Webhooks
Subscribe an HTTPS endpoint to changes in your account. Manage subscriptions in Settings, Integrations. Every delivery is a POST with a JSON event, signed with the secret shown once at creation.
Events
property.created
property.updated
contact.created
contact.updated
contract.created
contract.updated
task.created
task.updated
maintenance.created
maintenance.updated
A 2xx response marks the delivery as successful. On any other outcome we retry up to 5 times with exponential backoff (max 60 seconds between attempts), then mark the delivery as failed.
{ "id": "evt_9f1c…", "type": "property.updated", "createdAt": "2026-08-16T09:30:00.000Z", "data": { "id": 42, "address": "Grote Markt 1", "...": "…" } }
Verifying signatures
Every delivery sends the headers x-keypilot-event, x-keypilot-event-id, x-keypilot-timestamp and x-keypilot-signature. The signature is v1= followed by the hex HMAC-SHA256 of "<timestamp>.<raw body>" with your webhook secret. Compare timing-safe and process each event id at most once.
import crypto from "node:crypto"; // rawBody: the exact request body string, before JSON.parse function verifyKeypilotWebhook(rawBody, secret, timestamp, signature) { const expected = "v1=" + crypto.createHmac("sha256", secret) .update(timestamp + "." + rawBody) .digest("hex"); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); }
Endpoint reference
This reference is rendered directly from the OpenAPI specification, so it can never lag behind. Base URL for all v1 endpoints: https://www.keypilot.eu/api/v1.
Properties
Buildings and units for rent or sale.
GET
/api/v1/properties
properties:read
List properties
Returns your properties ordered by id, with cursor pagination.
Parameters:
limit (query), cursor (query)
POST
/api/v1/properties
properties:write
Create a property
Creates a property and emits a `property.created` webhook event.
Parameters:
Idempotency-Key (header)
GET
/api/v1/properties/{id}
properties:read
Retrieve a property
Parameters:
id (path)
PATCH
/api/v1/properties/{id}
properties:write
Update a property
Partial update: only the supplied fields change. Emits a `property.updated` webhook event.
Parameters:
id (path), Idempotency-Key (header)
Contacts
Tenants and contractors. Reads of this resource are audit-logged.
GET
/api/v1/contacts
contacts:read
List contacts
Returns your contacts ordered by id, with cursor pagination.
Parameters:
limit (query), cursor (query)
POST
/api/v1/contacts
contacts:write
Create a contact
Creates a contact and emits a `contact.created` webhook event. When `propertyId` is supplied it must reference one of your properties.
Parameters:
Idempotency-Key (header)
GET
/api/v1/contacts/{id}
contacts:read
Retrieve a contact
Parameters:
id (path)
PATCH
/api/v1/contacts/{id}
contacts:write
Update a contact
Partial update: only the supplied fields change. Emits a `contact.updated` webhook event.
Parameters:
id (path), Idempotency-Key (header)
Contracts
Rental contracts linked to properties and tenants.
GET
/api/v1/contracts
contracts:read
List contracts
Returns your contracts ordered by id, with cursor pagination.
Parameters:
limit (query), cursor (query)
POST
/api/v1/contracts
contracts:write
Create a contract
Creates a contract and emits a `contract.created` webhook event. `contractNumber` must be unique per account; new contracts are created as drafts.
Parameters:
Idempotency-Key (header)
GET
/api/v1/contracts/{id}
contracts:read
Retrieve a contract
Parameters:
id (path)
PATCH
/api/v1/contracts/{id}
contracts:write
Update a contract
Partial update: only the supplied fields change. Emits a `contract.updated` webhook event. `contractNumber` cannot be changed.
Parameters:
id (path), Idempotency-Key (header)
Tasks
To-dos, optionally linked to a property or contractor.
GET
/api/v1/tasks
tasks:read
List tasks
Returns your tasks ordered by id, with cursor pagination.
Parameters:
limit (query), cursor (query)
POST
/api/v1/tasks
tasks:write
Create a task
Creates a task and emits a `task.created` webhook event.
Parameters:
Idempotency-Key (header)
GET
/api/v1/tasks/{id}
tasks:read
Retrieve a task
Parameters:
id (path)
PATCH
/api/v1/tasks/{id}
tasks:write
Update a task
Partial update: only the supplied fields change. Emits a `task.updated` webhook event.
Parameters:
id (path), Idempotency-Key (header)
Maintenance requests
Tenant-reported or API-created maintenance tickets.
GET
/api/v1/maintenance-requests
maintenance:read
List maintenance requests
Returns your maintenance requests ordered by id, with cursor pagination. The resource path is `/api/v1/maintenance-requests`.
Parameters:
limit (query), cursor (query)
POST
/api/v1/maintenance-requests
maintenance:write
Create a maintenance
Creates a maintenance and emits a `maintenance.created` webhook event. `tenantId` is required and must reference one of your tenants.
Parameters:
Idempotency-Key (header)
GET
/api/v1/maintenance-requests/{id}
maintenance:read
Retrieve a maintenance
Parameters:
id (path)
PATCH
/api/v1/maintenance-requests/{id}
maintenance:write
Update a maintenance
Partial update: only the supplied fields change. Emits a `maintenance.updated` webhook event. `tenantId`, `propertyId` and `contractId` cannot be changed.
Parameters:
id (path), Idempotency-Key (header)
Credentials
API credential management. These endpoints use the web app session (cookie) authentication, not an API key.
GET
/api/integrations/api-credentials
session (web app)
List API credentials
Returns all credentials of the signed-in account, most recent first. Secrets are never returned after creation.
POST
/api/integrations/api-credentials
session (web app)
Create an API credential
The full secret (`kp_<prefix>_<secret>`) is returned exactly once, in this response.
POST
/api/integrations/api-credentials/{id}/rotate
session (web app)
Rotate an API credential
Revokes the credential and issues a new one with the same label and scopes. The new secret is returned once.
Parameters:
id (path)
DELETE
/api/integrations/api-credentials/{id}
session (web app)
Revoke an API credential
Parameters:
id (path)
Webhooks
Subscription management (session-authenticated) and the event envelope delivered to your endpoint. Deliveries are signed: `x-keypilot-signature: v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>"` with the subscription secret; verify with `x-keypilot-timestamp`. Failed deliveries retry up to 5 times with exponential backoff (max 60 s).
GET
/api/integrations/webhooks
session (web app)
List webhook subscriptions
POST
/api/integrations/webhooks
session (web app)
Create a webhook subscription
The URL must be HTTPS and resolve to a public address (validated again at every delivery). The signing secret is returned exactly once.
PATCH
/api/integrations/webhooks/{id}
session (web app)
Update a webhook subscription
Enable/disable a subscription, rename it or change the subscribed events.
Parameters:
id (path)
GET
/api/integrations/webhooks/{id}/deliveries
session (web app)
List recent deliveries
Returns the 100 most recent delivery attempts for a subscription, newest first.
Parameters:
id (path)
Machine-readable
For tooling, AI assistants and API catalogs, Keypilot publishes the specification at stable URLs.