Error handling

Every Lovelio API response - success or failure - carries a consistent envelope.

Success

{
  "success": true,
  "data": { ... },
  "meta": { "request_id": "req_abc123def456" },
  "error": null
}

Failure

{
  "success": false,
  "data": null,
  "meta": { "request_id": "req_abc123def456" },
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "email is required.",
    "field": "email",
    "docs": "https://lovelio.ai/docs/agents/error-handling",
    "request_id": "req_abc123def456"
  }
}

error.field is present only when a specific field is at fault. error.docs always points at the docs for that code.

Always log request_id

Every response includes meta.request_id (and repeats it on error.request_id for failures). Log it on every API call you make. If you file a support ticket or need the team to trace an issue, the request ID is the fastest path from your log line to our traces.

HTTP header form: X-Request-ID. Both are set on every response.

Error codes

Branch on error.code, never on the message text.

error.codeHTTPWhen it happensWhat to do
VALIDATION_ERROR422Missing or malformed fieldFix and retry. error.field points at the offender.
AUTHENTICATION_REQUIRED401Missing or invalid API keyCheck the Authorization: Bearer header. Keys are regional - the message says when a valid key hit the wrong region's domain.
INSUFFICIENT_SCOPE403API key scope does not cover this actionUse a key with the named scope or narrow the request.
ROUTE_NOT_FOUND404Unknown REST pathCheck the path in the API reference.
METHOD_NOT_ALLOWED405This REST path does not support that methodUse a method from the Allow header.
RESOURCE_NOT_FOUND404Resource does not exist, or belongs to another accountThe ID is wrong or not yours. Do not retry.
CONFLICT409Duplicate resource or state mismatchHandle as a no-op or pick a different identifier.
IDEMPOTENCY_KEY_REUSED409Same key, different request or authorization contextReconcile the original operation before using a new key.
IDEMPOTENCY_IN_PROGRESS409An identical request owns the key and is still runningWait for Retry-After, then retry the identical request with the same key.
IDEMPOTENCY_OUTCOME_UNKNOWN409A write may have completed without a saved responseDo not use a new key. Reconcile the target resource with a read, then contact support with X-Idempotency-Operation-ID if needed.
IDEMPOTENCY_UNAVAILABLE503Lovelio could not confirm the claim; no handler startedWait for Retry-After, then retry the identical request with the same key.
IDEMPOTENCY_RESULT_STORAGE_FAILED503The handler finished, but Lovelio could not confirm response storageRetry only with the same key. Never create a new key until the result is known.
MISSING_IDEMPOTENCY_KEY400POST to an idempotent endpoint without the headerAdd an Idempotency-Key header.
RATE_LIMIT_EXCEEDED429Too many requests this minuteRead Retry-After, back off, retry.
INTERNAL_ERROR500Our faultRetry with backoff. If persistent, file with the request_id.

Codes you will meet on specific paths: TRIAL_EXPIRED (403), EMAIL_NOT_VERIFIED (403, trial keys must verify email before writes), PREMIUM_REQUIRED (402), QUOTA_EXCEEDED (403, over a volume allowance - talk to us rather than wait), PAYLOAD_TOO_LARGE (413), ACCOUNT_NOT_FOUND (404), MARKETPLACE_DISABLED (403), ACCOUNT_SUSPENDED (403).

Two of those are about the agency, not your credential, and no retry or reconnect will clear either. PREMIUM_REQUIRED on authentication means the agency has no active plan: the account is closed to its own staff as well, and an admin turns it back on by signing in at lovelio.ai. ACCOUNT_SUSPENDED means Lovelio has made the agency dark. Stop calling and tell whoever owns the integration.

Idempotency

POST endpoints that create or send things require an Idempotency-Key header. Lovelio atomically claims the account-scoped key before the handler starts. Use one UUID per logical operation. An identical completed retry under the same credential and permissions within 24 hours returns the saved response with X-Idempotency-Replayed: true.

curl -X POST $LOVELIO_HOST/api/v1/applications/app_01HXXX/stage \
  -H "Authorization: Bearer $LOVELIO_API_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{ "stage": "yes", "reason": "Strong screen - moving forward" }'

Rules:

  • Keys apply to POST only. PATCH and DELETE ignore the header.
  • Completed results and unstarted claims expire after 24 hours. An interrupted or uncertain execution remains reserved beyond that window; expiry never makes an uncertain write safe to repeat.
  • A key is bound to the method, path, query and logical content. JSON object order does not matter.
  • Multipart content includes every field, record id, filename, content type and file byte hash. Random boundary values do not matter.
  • A changed request returns 409 IDEMPOTENCY_KEY_REUSED. It never receives the first request's response.
  • Concurrent identical calls return the saved response or 409 IDEMPOTENCY_IN_PROGRESS. Follow Retry-After with the same key.
  • X-Idempotency-Operation-ID identifies the durable claim. Log it with the request ID.

Permission-bound retry receipts

Retry receipts bind to the API credential, acting user, role, granted scopes and data scope. Refreshing an access token for the same connection preserves that binding. A different connection or changed access cannot read the old cached response.

Older receipts without this authorization binding cannot replay. They return IDEMPOTENCY_KEY_REUSED without running the operation again. Read the affected record to reconcile the original write; do not create a new key just to bypass this refusal. Keep the original key and operation ID for support.

Retry rules

Retry 429 after Retry-After. Reads can retry 500 and 503 with backoff. An idempotent POST must keep the same key and content. If a write outcome is uncertain, reconcile first; never repeat it with a new key. PATCH and streaming writes do not gain retry protection from an idempotency header.

Retry IDEMPOTENCY_IN_PROGRESS with the same key after Retry-After. Retry IDEMPOTENCY_UNAVAILABLE and IDEMPOTENCY_RESULT_STORAGE_FAILED only with the same key.

Do not retry IDEMPOTENCY_OUTCOME_UNKNOWN with a new key, even after 24 hours. Read the target collection or resource first. Use the operation ID when support must reconcile it. Legacy uploads and rows without a body fingerprint also fail closed because the original logical request cannot be proven.

Do not retry other 4xx responses unchanged.

Exponential backoff starting at 1 second, doubling to a max of 60. Stop after 5 attempts.

Rate limits

Authenticated requests that reach the rate limiter include:

X-RateLimit-Limit: 600
X-RateLimit-Remaining: 598
X-RateLimit-Reset: 1730000000

Limits are per API key, per minute window: 600 requests/minute on an active plan, 60 requests/minute for trial, past-due, and cancelled accounts. There is no burst allowance - the minute window is the whole rule.

When you hit 429, Retry-After is the seconds until the window resets. Respect it.

Bulk operations

If you find yourself making many calls in a row, switch to POST /api/v1/batch. One call dispatches up to 100 canonical actions and counts as a single rate-limit unit.

curl -X POST $LOVELIO_HOST/api/v1/batch \
  -H "Authorization: Bearer $LOVELIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      { "op": "move_stage", "payload": { "application_id": "app_1", "new_stage": "rejected" }, "idempotency_key": "batch-001" },
      { "op": "move_stage", "payload": { "application_id": "app_2", "new_stage": "yes" }, "idempotency_key": "batch-002" }
    ]
  }'

Operations run independently - one failing never rolls back the others. Each key is bound to its action and payload. Check each status and error code. Retry only the failed operation with its original key and payload when its code permits a retry.

Debugging checklist

When a call fails and the error message is not obvious:

  1. Check error.docs - opens the public REST error guide.
  2. Copy the request_id. Paste it into any support message.
  3. Verify the key is live: GET /api/v1/accounts/me. If that returns 200, your auth is fine; the issue is in the specific endpoint.
  4. Verify the resource exists and belongs to the authed account: GET /api/v1/{resource}/{id}. A 404 here means wrong account or deleted resource.
  5. For async tasks, check the task state: GET /api/v1/tasks/{task_id}. Error details live there, not on the originating call.

Correct a first-call error

Use the base URL shown in the portal, including /api/v1. Staging sandbox keys belong on staging; production keys belong on their region's domain. Set LOVELIO_API_URL to that base and LOVELIO_SANDBOX_KEY to your full sandbox key in your terminal.

# Intentional typo: expect 404 ROUTE_NOT_FOUND with error.docs and a request ID.
curl -sS "$LOVELIO_API_URL/no-such-endpoint" \
  -H "Authorization: Bearer $LOVELIO_SANDBOX_KEY"
# Correct the path: expect 200, success: true and an array of jobs.
curl -sS "$LOVELIO_API_URL/jobs?limit=1" \
  -H "Authorization: Bearer $LOVELIO_SANDBOX_KEY"

Unknown REST paths return JSON 404. Unsupported REST methods return JSON 405 with an Allow header. HEAD returns headers without a body; OPTIONS keeps its HTTP preflight behavior. These rules cover /api/v1 and /v1. OAuth uses its OAuth error format; MCP uses its own transport and JSON-RPC errors. A 405 on MCP GET can be valid when the server does not offer an event stream.

The error envelope has success: false, meta.request_id, and error.code, message, docs and request_id. The two request IDs match. error.field is optional. data is usually null; a failed draft confirmation can include structured details for correction.

MCP tool errors

MCP tools preserve these API codes in structuredContent.error.code and mark failures with isError: true. They include a category and a retry instruction. Existing clients continue to receive readable text. Mixed batches retain every operation and flag any failure. Follow the MCP result contract, including its rules for uncertain writes.