Every request needs a credential and every response is JSON.
1. Get a credential
There are two kinds, and which one you want depends on whose data you are reading.
An API key is for your own scripts and servers, acting on your own workspace. Create one in Settings → Developers, pick the scopes it needs, and send it as:
Authorization: token reos_live_…
OAuth2 is for an application acting on other workspaces' behalf. Register the app in Settings → Developers, then send people through the authorization flow. See Authentication.
2. Check it works
curl https://app.zellerai.co/api/v1/ping \
-H "Authorization: token reos_live_…"
{
"ok": true,
"tenant_id": "clx…",
"credential": "api_key",
"scopes": ["contacts:read", "messages:send"],
"rate_limit_per_min": 120,
"api_version": "v1"
}
ping needs no scope beyond a valid credential. If it works and something else returns 403, the problem is the scope, not the key — and the 403 body names exactly which scope was missing.
3. Read something
curl "https://app.zellerai.co/api/v1/contacts?limit=5" \
-H "Authorization: token reos_live_…"
The three things that will surprise you
Phone numbers and email addresses come back masked unless your credential holds contacts:read:pii. Every payload carries pii_masked so you can tell a masked value from an absent one. This is deliberate: "see your contacts" and "download every client's mobile number" are different permissions and our consent screen asks them separately.
Sending a message can be refused. POST /messages runs the same compliance gate the product runs — consent, suppression, opt-out, quiet hours, A2P registration. A refused send is a 422 compliance_blocked naming the reason, and the attempt is still recorded in the workspace's inbox. There is no flag that turns this off. See Compliance.
DELETE /contacts/{id} anonymizes; it does not delete the row. A contact is referenced by deals, commissions and messages a brokerage is required to keep.