Gary Club
Authentication

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.

Why no scopes?
Scopes added drift between the dashboard and the docs without ever catching a real bug. Tenancy isolation does the actual work — an agency key never sees another agency's data; a client key never sees another client's. We removed the scope catalog so the surface matches reality: one key, one tenancy, full access inside it.

Bearer token#

Pass your API key on every request as a Bearer token. Both REST and the MCP server use the same header.

Standard request
GET /api/public/v1/me HTTP/1.1
Host: agency.gary.club
Authorization: Bearer gc_live_EXAMPLE_AbCdEfGhIj0123456789

The two key shapes#

gc_live_…Agency
Org-wide. Default tenant scope is the agency's own CRM (the agency-self sentinel). Pass X-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_…Client
Bound to a single client at mint time. Cross-client requests return 404 — 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).
Where they live in the dashboard
Agency key: Settings → API Keys on 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.

Agency key acting as a client
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 header
Tenant scope = agency-self CRM. Same as if you were using the agency UI.
gc_live_ + X-Gary-Client-Id
Tenant scope = (org_id, that client_id). All reads/writes confined to that client.
cl_live_ + no header
Tenant scope = the bound (org_id, client_id). The header is unnecessary.
cl_live_ + X-Gary-Client-Id
Rejected with 400 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.

Narrow a list to one agent
GET /api/public/v1/calls?agent_id=agent_3kf9ab&limit=20 HTTP/1.1
Host: agency.gary.club
Authorization: Bearer cl_live_EXAMPLE_AbCdEfGhIj0123456789

Rate limits#

Per-key, sliding 60-second window. Default 60 requests/minute; up to 600/min with admin approval. Every response carries live counters.

Response headers
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1735000000

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

401 Unauthorized
{
  "error": "invalid_api_key",
  "message": "API key is invalid"
}
authentication_required401
Missing or malformed Authorization header.
invalid_api_key401
Header parsed but key not found in our DB or has the wrong shape.
revoked_api_key401
Key was revoked from the dashboard.
expired_api_key401
Key's expires_at is in the past.
invalid_request400
Client-bound key tried to override tenancy with X-Gary-Client-Id. Client keys are sealed to their client.
rate_limited429
Per-key sliding 60-second window exceeded. Honour Retry-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.

If a key leaks
Hit Rotate immediately. The 5-minute grace gives you a chance to update integrations; if the leak is active, force-revoke the old key from the dropdown next to it. Audit recent requests in the Activity log to see what was done with it.