Rate limits & errors
The API is predictable on purpose: one error envelope, conventional HTTP statuses, stable snake_case codes, and rate-limit headers on every response.
Rate limits#
Each key has a token bucket of 60 requests per minute by default, refilled continuously (so a burst of 60 is fine, sustained traffic settles at 60/min). Limits are per key, not per organization: two keys get two buckets. A higher per-key limit can be set when the key is created — ask your Lojiq contact.
Every authenticated response carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests per minute allowed for this key. |
X-RateLimit-Remaining | Requests left right now (rounded down). |
Retry-After | Only on 429: seconds to wait. |
X-Request-Id | Unique id of the request; include it in support emails. |
{ "error": "rate_limited", "message": "This key is limited to 60 requests per minute. Retry in 3s.", "retry_after_seconds": 3, "request_id": "0f3b9c1e-…" }
Handle 429 by sleeping Retry-After seconds and retrying; do not retry in a tight loop. Bulk work belongs in POST /leads/bulk (one request for thousands of rows), not in thousands of POST /leads.
Starting calls has a second, separate limit: your organization's concurrent-call cap. When it is reached, POST /calls and POST /agents/{id}/calls answer 429 concurrency_limit_reached (or concurrency_limit from the voice provider). Retry after a few seconds; no header is sent for this one.
Pricing of requests#
API requests themselves are free. Calls and texts you start through the API bill tokens exactly as they would from the app, on completion. Creation of a call is refused with 402 insufficient_balance when the balance is exhausted; check GET /account → voice_calls_allowed before a batch.
The error envelope#
{
"error": "insufficient_scope", // stable, machine-readable, snake_case
"message": "This key does not have the leads:write scope.", // optional plain words
"request_id": "0f3b9c1e-6d2a-4b7f-8e1c-5a9d2b4c6e80", // same value as the X-Request-Id header
"required": "leads:write" // extras on a few codes (see below)
}
Every non-2xx answer has this shape, 401s and 429s included. Extras: required on insufficient_scope, retry_after_seconds on rate_limited, docs on a 404 for an unknown path.
Some codes carry a suffix after a colon: unknown_field:nickname, unknown_scope:foo, unknown_event_type:x.y, unknown_template_field:tone. Match on the part before the colon.
On a 400 invalid_request about your own input, message may carry the storage engine's wording ("invalid input syntax for type uuid"). Lojiq's own failures are always 500 internal_error with a generic message; the detail is in Lojiq's log under your request_id. Treat any error you do not recognise as a generic failure of that status class.
HTTP status conventions#
| Status | When | What to do |
|---|---|---|
200 | Read or update succeeded. Also POST /leads when it updated an existing lead. | — |
201 | Created (lead, contact, agent, call, webhook). | — |
202 | POST /leads/bulk finished processing; the body has the counts. | Inspect failed. |
302 | GET /calls/{id}/recording redirects to the audio. | Follow without the API key header. |
400 | Validation failed: unknown field, wrong type or range, bad phone number, invalid JSON. | Fix the request. Do not retry unchanged. |
401 | Key missing, unknown, revoked or expired. | Fix the key. Do not retry unchanged. |
402 | Token balance exhausted (starting calls, resuming campaigns). | Top up in the app. |
403 | Missing scope, organization not active, voice suspended, or destination on your DNC list. | See the code. |
404 | No such resource in your organization (reads and updates alike); no recording; a campaign_id or lead_id that is not yours; an unknown path. | — |
409 | State conflict: campaign not paused/active, call already ended, agent managed in app, number in use, no caller id available, an Idempotency-Key still in flight… | Read the code; some are retryable after a change. |
413 | Request body too large (CSV over 5,000,000 bytes). | Split the file. |
422 | An Idempotency-Key was reused with a different body. | Use a new key. |
429 | Rate limit or concurrency cap. | Wait, then retry. |
500 | Lojiq-side failure (internal_error and a few named codes on calls). | Retry with exponential backoff, then contact support with X-Request-Id. |
501 | feature_unavailable: the endpoint is not available on your account yet (contacts, deals). | Use the alternative the reference names. |
502 / 503 | Carrier or voice provider unavailable; no carrier configured. | Retry later. |
Retry safety#
GET,PATCHandDELETEare safe to retry.POST /leads,POST /callsandPOST /agents/{id}/callstake anIdempotency-Keyheader (1–255 printable characters; a UUID is ideal). The first request runs and its answer is remembered for 24 hours; a retry with the same key and body gets that answer back withIdempotency-Replayed: trueand runs nothing — no second lead, no second paid call. The same key with a different body answers422 idempotency_key_reused; the same key while the first request is still running answers409 idempotency_in_progresswithRetry-After: 1. A5xxis not remembered, so retry it with the same key. Always send one when you start a call.- Without a key,
POST /leadsis still safe to retry (it deduplicates by phone and answers200), but a retriedPOST /callsplaces a second call. POST /leads/bulkis not blind-retry safe on a timeout: rows already processed would be updated again (harmless) and emitlead.updatedagain. Prefer smaller files.POST /webhookscreates a new subscription (and secret) every time; list first.
Every error code#
Grouped by where it comes from. The reference page of each endpoint lists exactly which of these it can return.
Everywhere#
invalid_request (400, message says what) · invalid_json (400) · unknown_field:<name> (400) · nothing_to_update (400) · not_found (404) · conflict (409, a uniqueness rule) · payload_too_large (413) · internal_error (500) · feature_unavailable (501)
Authentication and limits#
missing_api_key · invalid_api_key · revoked_api_key · expired_api_key · auth_lookup_failed · insufficient_scope · unauthenticated · rate_limited
Idempotency#
invalid_idempotency_key (400) · idempotency_in_progress (409) · idempotency_key_reused (422)
Leads & CSV#
invalid_phone_number · phone_number_immutable · campaign_not_found (404) · csv_required · invalid_csv · import_failed (500) · export_failed (500) · per-row (in the import job, not the response): invalid_or_missing_phone, missing_required:<column>
Campaigns#
campaign_not_found (404) · campaign_not_resumable (409) · campaign_not_active (409) · insufficient_balance (402)
Calls and voice agents#
medium_required · invalid_medium · invalid_to_number · invalid_from_number · from_number_not_owned · system_prompt_required · system_prompt_must_be_string · system_prompt_too_long · call_template_must_be_object · unknown_template_field:<key> · unknown_voice · temperature_out_of_range · invalid_first_speaker · recording_enabled_must_be_boolean · greeting_must_be_string · greeting_too_long · max_duration_out_of_range · template_context_must_be_object · template_context_too_many_keys · template_context_values_must_be_scalar · template_context_value_too_long · metadata_must_be_object · metadata_too_many_keys · metadata_values_must_be_scalar · metadata_value_too_long · name_required · name_too_long · nothing_to_update · agent_managed_in_app · agent_in_use · agent_has_no_call_template · orchestrated_agent_not_supported · phone_number_required · phone_number_not_owned · phone_number_in_use · organization_not_found · organization_not_active · voice_suspended · insufficient_balance · destination_on_dnc · lead_not_found · lead_lookup_failed · org_lookup_failed · concurrency_limit_reached · concurrency_limit · no_did_available · no_voice_carrier_available · voice_carrier_blocked · carrier_dial_failed · call_creation_failed · call_record_failed · voice_provider_unavailable · call_already_ended · call_not_connected · hangup_failed · recording_not_available · preview_failed · not_provisioned
Webhooks#
event_types_required · unknown_event_type:<type> · unknown_field:<name> · malformed_url · invalid_protocol · https_required_in_production · userinfo_not_allowed · missing_host · private_hostname · private_ipv4 · private_ipv6 · dns_lookup_failed · dns_no_records · private_resolved_ipv4 · private_resolved_ipv6
Reference files#
spec_unavailable · guide_unavailable · sdk_unavailable