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:

HeaderMeaning
X-RateLimit-LimitRequests per minute allowed for this key.
X-RateLimit-RemainingRequests left right now (rounded down).
Retry-AfterOnly on 429: seconds to wait.
X-Request-IdUnique id of the request; include it in support emails.
429 Too Many Requests
{ "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#

json
{
  "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#

StatusWhenWhat to do
200Read or update succeeded. Also POST /leads when it updated an existing lead.—
201Created (lead, contact, agent, call, webhook).—
202POST /leads/bulk finished processing; the body has the counts.Inspect failed.
302GET /calls/{id}/recording redirects to the audio.Follow without the API key header.
400Validation failed: unknown field, wrong type or range, bad phone number, invalid JSON.Fix the request. Do not retry unchanged.
401Key missing, unknown, revoked or expired.Fix the key. Do not retry unchanged.
402Token balance exhausted (starting calls, resuming campaigns).Top up in the app.
403Missing scope, organization not active, voice suspended, or destination on your DNC list.See the code.
404No such resource in your organization (reads and updates alike); no recording; a campaign_id or lead_id that is not yours; an unknown path.—
409State 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.
413Request body too large (CSV over 5,000,000 bytes).Split the file.
422An Idempotency-Key was reused with a different body.Use a new key.
429Rate limit or concurrency cap.Wait, then retry.
500Lojiq-side failure (internal_error and a few named codes on calls).Retry with exponential backoff, then contact support with X-Request-Id.
501feature_unavailable: the endpoint is not available on your account yet (contacts, deals).Use the alternative the reference names.
502 / 503Carrier or voice provider unavailable; no carrier configured.Retry later.

Retry safety#

  • GET, PATCH and DELETE are safe to retry.
  • POST /leads, POST /calls and POST /agents/{id}/calls take an Idempotency-Key header (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 with Idempotency-Replayed: true and runs nothing — no second lead, no second paid call. The same key with a different body answers 422 idempotency_key_reused; the same key while the first request is still running answers 409 idempotency_in_progress with Retry-After: 1. A 5xx is not remembered, so retry it with the same key. Always send one when you start a call.
  • Without a key, POST /leads is still safe to retry (it deduplicates by phone and answers 200), but a retried POST /calls places a second call.
  • POST /leads/bulk is not blind-retry safe on a timeout: rows already processed would be updated again (harmless) and emit lead.updated again. Prefer smaller files.
  • POST /webhooks creates 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