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_required401Missing or malformed
Authorization header. Don't retry — fix the header.invalid_api_key401Header parsed but key not found in our DB or wrong shape. Don't retry — mint a new key.
revoked_api_key401Key was revoked from the dashboard. Don't retry — issue a replacement.
expired_api_key401expires_at is in the past. Don't retry — issue a fresh key.
fuel_exhausted402Out of FUEL.
details.required + details.balance tell you the gap. Top up at agency.gary.club/dashboard/billing — don't retry on a loop.forbidden403Key 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_disabled403A feature gate is closed (e.g.
ai_sdr_outbound_enabled = false). Don't retry — flip the gate from the agency dashboard.forbidden403Generic refusal where no more specific code applies.
not_found404The resource doesn't exist OR isn't in your tenant. We don't leak existence; cross-tenant reads return 404 not 403.
invalid_request400Body is missing a required field, has the wrong type, or violates a validation rule.
details usually tells you which field.unprocessable422Request 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.
conflict409State collision — most often an Idempotency-Key replay against a different body. Either reuse the original key+body or use a fresh key.
rate_limited429Per-key 60-second window exceeded. Honour
Retry-After; backoff before retrying.internal_error500Something 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#
5xxYes — exponential backoff (e.g. 1s, 2s, 4s, 8s up to a cap). After ~5 attempts, escalate.
429Yes, 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_exhaustedNo automatic retry. Surface the error to a human; top up FUEL; resume.
Logging tips#
- Log the response's
X-Gary-Request-Idheader (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-Keyso a replayed request can be correlated.

