Authentication
Two key shapes — one for the agency, one per client. Full access at the tenancy boundary. Bearer auth on every endpoint.
The model in one paragraph#
Every key is either an agency key (gc_live_…) that can read and write across all of your clients, or a client key (cl_live_…) sealed to a single client at mint time. There are no scopes — a key has full access inside its tenancy boundary. The boundary is what protects you, not a permission list.
Bearer token#
Pass your API key on every request as a Bearer token. Both REST and the MCP server use the same header.
GET /api/public/v1/me HTTP/1.1
Host: agency.gary.club
Authorization: Bearer gc_live_EXAMPLE_AbCdEfGhIj0123456789The two key shapes#
gc_live_…AgencyX-Gary-Client-Id per-request to act as a specific client. One per agency by default — rotate to issue a new one and retire the old.cl_live_…Client404 — no leakage possible. One per client. Hand it to the integration that should only see that customer's data (n8n flow, Zapier connection, custom dashboard).agency.gary.club. Client keys: each client's API Keys tab. Both pages show the primary key with a copy button and a single Rotate action — no per-scope checkboxes, no key zoo.Acting as a client#
With a gc_live_ key, pass X-Gary-Client-Id per-request to scope the call to one client. The header is the only thing that distinguishes agency-self writes from client-scoped writes.
POST /api/public/v1/crm/contacts HTTP/1.1
Host: agency.gary.club
Authorization: Bearer gc_live_EXAMPLE_AbCdEfGhIj0123456789
X-Gary-Client-Id: aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa
Content-Type: application/json
{ "email": "lead@example.com", "first_name": "Sample", "last_name": "Lead" }gc_live_ + no headergc_live_ + X-Gary-Client-Idcl_live_ + no headercl_live_ + X-Gary-Client-Id400 invalid_request — key is already client-bound, can't be redirected.Agent narrowing (optional)#
Several endpoints accept an ?agent_id=… query param to narrow results to a specific agent — its own conversations, calls, and CRM rows it owns. Use this when one client has multiple agents (an inbound voice agent + an outbound dialer + an SMS bot) and an integration only needs visibility into one of them. The key still scopes the tenancy; agent_id just narrows within it.
GET /api/public/v1/calls?agent_id=agent_3kf9ab&limit=20 HTTP/1.1
Host: agency.gary.club
Authorization: Bearer cl_live_EXAMPLE_AbCdEfGhIj0123456789Rate limits#
Per-key, sliding 60-second window. Default 60 requests/minute; up to 600/min with admin approval. Every response carries live counters.
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1735000000On 429 we set Retry-After in seconds. Implement a small exponential backoff if you're hitting the limit consistently; request a quota bump if it's a sustained workload.
Auth errors#
All errors share the same shape — branch on the machine code, never the message:
{
"error": "invalid_api_key",
"message": "API key is invalid"
}authentication_required401Authorization header.invalid_api_key401revoked_api_key401expired_api_key401expires_at is in the past.invalid_request400X-Gary-Client-Id. Client keys are sealed to their client.rate_limited429Retry-After.See Errors for the full catalog (4xx + 5xx) and what each one means.
Rotating keys#
Both the agency and per-client API Keys pages have a single Rotate action. Hitting it mints a new key, displays it once, and the previous key keeps working for a 5-minute grace window so you can swap it into your integration before the old one stops responding. After 5 minutes the previous key returns 401 revoked_api_key.

