Errors & recovery
Distinguish rejected requests, failed searches, and features still planned for production.
Implemented error mapping
| HTTP / outcome | Code or shape | Meaning | Your next step |
|---|---|---|---|
| 401 | error.code: "invalid_api_key" | The API key is missing, invalid, revoked, or expired. | Send a valid dashboard key. No search is created. |
| 402 | error.code: "insufficient_balance" | Your credit balance cannot cover this search's estimated_cost. | Top up or reduce the requested work. No search is created and nothing is deducted. |
| 404 | error.code: "job_not_found" | No retained record for this account, or the ID belongs to another account. | Verify the ID and key. The result may have expired after one hour, been deleted, or been lost. |
| 409 | error.code: "search_conflict" | The search is already running or its state changed during a write. | Read the known search ID; do not duplicate work automatically. |
| 503 | error.code: "cache_unavailable" | Redis could not confirm a read or write. | Retry GET with backoff when the ID is known; POST outcomes may be uncertain. |
| 429 | error.code: "rate_limit_exceeded" | This API key exceeded its per-minute or per-hour limit. | Wait for the Retry-After header, then retry the same request. No search is created. |
| 503 | error.code: "service_unavailable" | API key verification or rate limiting could not be completed. | Retry later. No search is created. |
| 422 | error.code: "invalid_webhook_url" | webhook_url is not a public http(s) URL. | Send a public URL, or omit it and poll. No search is created. |
| 422 | detail: [...] | Invalid JSON, an unknown field, or a value outside the request schema. | Correct the fields identified in detail[].loc. |
202 creation, then 200 polling with status: "failed" | Failed SearchResult | Search execution failed, exceeded its execution timeout, or its worker stopped with a recorded failure. | Treat the search as failed; do not interpret HTTP 200 as successful retrieval. |
Insufficient balance
Creating a search returns 402 when the account cannot cover estimated_cost. Branch on error.code. The message states the reason. details.estimated_cost is the price that was compared. details.credits_remaining is your remaining credit balance.
{
"error": {
"code": "insufficient_balance",
"message": "Prepaid balance is below the estimated cost of this search.",
"details": {
"estimated_cost": 0.165,
"credits_remaining": 0.04
}
}
}No search is created and nothing is charged. Add credits on the API and MCP page in Sourcely, or turn on automatic top-up, then send the request again. A cheaper request, such as enrichment: "none", also has a lower estimate.
Not found
{
"error": {
"code": "job_not_found",
"message": "Job src_missing was not found.",
"details": null
}
}Cache unavailable
{
"error": {
"code": "cache_unavailable",
"message": "Search storage is temporarily unavailable.",
"details": null
}
}A confirmed admission failure before the Redis transaction executes prevents provider execution. A lost acknowledgement or transaction command failure can leave an uncertain outcome; a completion write failure can occur after providers ran. Neither case is automatically retried by the API. If you have the search ID, retry GET with bounded backoff. Without an ID, repeating POST may start duplicate work; request-level idempotency is still planned. A failed search is reported with actual_cost 0. A completed search is reported at its calculated cost. A create whose estimate is above the credit balance returns 402 and does not start a search.
Validation
For example, max_claims: 0 is outside the accepted range. The validation envelope differs from the custom API error envelope:
{
"detail": [
{
"loc": ["body", "max_claims"],
"msg": "Input should be greater than or equal to 1",
"type": "greater_than_equal",
"input": 0,
"ctx": { "ge": 1 }
}
]
}Additional details and message text can vary. Branch on the status and structured fields, rather than matching the full message. Validation responses can include the invalid input; avoid recording submitted text or credentials in application logs.
Failed job
{
"job_id": "src_example",
"status": "failed",
"result_type": "search",
"document": null,
"claims": null,
"sources": {},
"usage": {
"currency": "USD",
"estimated_cost": 0.005,
"actual_cost": 0,
"discount_percent": 0,
"balance_after": null
},
"metadata": null
}The current SearchResult has no job-level error field. A failed claim may have its own error, but stored job-level error details are not exposed yet.
Empty results versus failures
A completed search may contain no sources. Current provider adapters also catch upstream failures and return empty lists, so the preview cannot always distinguish no matches from an exhausted provider failure. Clear provider-failure classification is planned.
Planned production errors
The following are design targets, not currently enforced responses for the initial pair of endpoints:
| HTTP | Proposed code | Planned action | Planned balance effect |
|---|---|---|---|
| 403 | account_disabled | Contact support. | No deduction. |
| 409 | idempotency_conflict | Restore the original request for that key. | No extra deduction. |
| 410 | search_expired | The retained result has expired, when a tombstone confirms this. | No automatic rerun or deduction. |
| 429 | concurrency_limit_exceeded | Too many searches are already running for this key. | Back off according to Retry-After. Rejected work consumes nothing. |
| Failed job | upstream_error / search_timeout / internal_error | Inspect the recorded terminal outcome. | Release the hold or refund that failed operation. |
Safe recovery and balance
A dropped connection is not proof that a search failed. Recover an existing search ID when available. In the planned prepaid model, service failures release or refund the affected operation; successful work is not charged again when retrieved. Wallet accounting and automatic refunds are not implemented in the current API.