Gary Club
Errors

Errors

One shape, machine-readable codes. Branch on the code, never the message — messages are tuned for humans and may change.

Error shape#

Every non-2xx response uses the same JSON envelope:

Error shape
{
  "error":   "validation_error",
  "message": "Either email or phone is required",
  "details": {}
}
Branch on `error`, not `message`
The error field is a stable machine code we promise not to rename. message is for humans and may evolve. details is structured context that varies by code (always an object when present).

Error catalog#

authentication_required401
Missing or malformed Authorization header. Don't retry — fix the header.
invalid_api_key401
Header parsed but key not found in our DB or wrong shape. Don't retry — mint a new key.
revoked_api_key401
Key was revoked from the dashboard. Don't retry — issue a replacement.
expired_api_key401
expires_at is in the past. Don't retry — issue a fresh key.
fuel_exhausted402
Out of FUEL. details.required + details.balance tell you the gap. Top up at agency.gary.club/dashboard/billing — don't retry on a loop.
forbidden403
Key valid but the action is not allowed for this tenancy — typically a per-client (cl_live_…) key trying to touch an agency-level resource (e.g., SDR campaigns, prospects). Switch to an agency (gc_live_…) key.
feature_disabled403
A feature gate is closed (e.g. ai_sdr_outbound_enabled = false). Don't retry — flip the gate from the agency dashboard.
forbidden403
Generic refusal where no more specific code applies.
not_found404
The resource doesn't exist OR isn't in your tenant. We don't leak existence; cross-tenant reads return 404 not 403.
invalid_request400
Body is missing a required field, has the wrong type, or violates a validation rule. details usually tells you which field.
unprocessable422
Request shape was valid but the business rule rejected it (e.g. private-IP webhook URL, list system-managed lock). Read the message — don't retry blindly.
conflict409
State collision — most often an Idempotency-Key replay against a different body. Either reuse the original key+body or use a fresh key.
rate_limited429
Per-key 60-second window exceeded. Honour Retry-After; backoff before retrying.
internal_error500
Something on our side broke. Safe to retry with backoff. If it persists past a few attempts, ping support with the request id we log server-side.

What to retry#

5xx
Yes — exponential backoff (e.g. 1s, 2s, 4s, 8s up to a cap). After ~5 attempts, escalate.
429
Yes, but only after waiting Retry-After seconds. Implement a token-bucket if you're bursting.
4xx (other)
No. Fix the request. The body tells you what's wrong.
402 fuel_exhausted
No automatic retry. Surface the error to a human; top up FUEL; resume.

Logging tips#

  • Log the response's X-Gary-Request-Id header (set on every response) when something goes sideways. We can find your exact request in our logs from that id.
  • Don't log the bearer key. If you must, redact to gc_live_…<last4>.
  • For idempotent POSTs, also log your Idempotency-Key so a replayed request can be correlated.