Skip to main content
Six rules that hold for every endpoint in the reference.

Base URL and auth

Two kinds of token work: a zpl_ key from Settings, Integrations, API keys, or a Clerk OAuth token. The workspace comes from the credential, never from a parameter.
GET /v1/health and GET /v1/openapi.json are open.A key belongs to one workspace. A Clerk token gets its workspace from the organization you picked at consent.

Scopes

A key carries the scopes you ticked when you made it. A missing scope is a 403 forbidden. A person on OAuth has every scope their membership allows.
admin exists inside the product but is never handed out on a key. A key that could revoke keys is refused by the route and by the screen that issues them.contacts is part of the API vocabulary. The key screen currently offers read, write and runs. Without contacts the contact fields are absent, not null.

Paging

Every list answers { "data": [...], "next_cursor": "..." }. Pass limit (default 50, maximum 200) and send next_cursor back as ?cursor=. Stop when it is null.
A single object answers { "data": { ... } }. GET /v1/people adds total, which is how many rows match your filters before paging.The cursor is opaque. Do not parse one or construct one.Filters take several values comma separated: ?country=NL,BE. Dates are UTC days as YYYY-MM-DD. Booleans are literally true or false.An unknown query parameter is a 400. A filter typo never reads as a filtered list.

Idempotency

Every POST that creates something requires an Idempotency-Key. Any client-chosen value works, a UUID for instance. PATCH and DELETE do not take the header.
The same request twice with the same key has one effect and returns the same response twice.The same key with a different body is a 409 conflict. So is a repeat while the first request is still in flight. A POST without the header is a 400.

Rate limits

60 requests a minute per credential by default, or your plan’s limit if it is lower. The REST API and MCP share one bucket per credential.
Over the limit is a 429 rate_limited with a Retry-After header in seconds.Retry-After is the time until one request is allowed, not until the bucket is full. RateLimit-Reset is the time until it is full again. Back off on Retry-After.

Errors

code is the contract and is stable. message is for humans and may change. The full list is on Errors.
The REST API sits behind the api_keys feature and MCP behind mcp. A plan with neither answers 402 feature_not_in_plan on every call.details then carries feature and an upgrade_url. A usage cap that was exceeded answers 402 limit_exceeded in the same shape. Show the URL rather than retrying.