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://docs.lovelio.ai/errors/VALIDATION_ERROR",
"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.code | HTTP | When it happens | What to do |
|---|---|---|---|
VALIDATION_ERROR | 422 | Missing or malformed field | Fix and retry. error.field points at the offender. |
AUTHENTICATION_REQUIRED | 401 | Missing or invalid API key | Check the Authorization: Bearer header. Keys are regional - the message says when a valid key hit the wrong region's domain. |
INSUFFICIENT_SCOPE | 403 | API key scope does not cover this action | Use a key with the named scope or narrow the request. |
RESOURCE_NOT_FOUND | 404 | Resource does not exist, or belongs to another account | The ID is wrong or not yours. Do not retry. |
CONFLICT | 409 | Duplicate resource or state mismatch | Handle as a no-op or pick a different identifier. |
IDEMPOTENCY_KEY_REUSED | 409 | Same Idempotency-Key, different request | Use a fresh key for each distinct operation. |
MISSING_IDEMPOTENCY_KEY | 400 | POST to an idempotent endpoint without the header | Add an Idempotency-Key header. |
RATE_LIMIT_EXCEEDED | 429 | Too many requests this minute | Read Retry-After, back off, retry. |
INTERNAL_ERROR | 500 | Our fault | Retry 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).
Idempotency
POST endpoints that create or send things require an Idempotency-Key header - the OpenAPI spec marks the header required on each one, and posting without it returns 400 MISSING_IDEMPOTENCY_KEY. Use a UUID per logical operation. Retrying with the same key within 24 hours returns the original response (with an X-Idempotency-Replayed: true header) instead of running the operation again.
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.
- Keys are scoped to your account and live for 24 hours. After that a retry runs as a fresh operation.
- A key is bound to the exact request it was first used for (method, path, and body). Reusing it for a different request returns 409
IDEMPOTENCY_KEY_REUSED- it never replays the wrong response.
Retry rules
Safe to retry: any 5xx, any 429, any network error.
Safe to retry on POST because of the idempotency key: the retry returns the original response instead of creating a duplicate.
Never retry: 4xx other than 429. They will not succeed no matter how many times you try.
Exponential backoff starting at 1 second, doubling to a max of 60. Stop after 5 attempts.
Rate limits
Every response includes:
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, and each carries its own idempotency_key so you can retry a single failed op without re-running the ones that succeeded. The full action catalogue is on the POST /batch operation in the API reference.
Debugging checklist
When a call fails and the error message is not obvious:
- Check
error.docs- points to the docs for that error code. - Copy the
request_id. Paste it into any support message. - 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. - Verify the resource exists and belongs to the authed account:
GET /api/v1/{resource}/{id}. A 404 here means wrong account or deleted resource. - For async tasks, check the task state:
GET /api/v1/tasks/{task_id}. Error details live there, not on the originating call.