Appearance
Errors
Status: pilot. Draft — needs review before publishing.
Envelope
Every error uses one shape:
json
{
"error": {
"code": "insufficient_balance",
"message": "Human-readable summary.",
"details": {}
}
}code is the machine-readable value — branch on it, not on message, which is written for a person and may change. details carries field-level or domain-level specifics and may be empty.
Status codes
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_input, unsupported_image | Validation failed, unusable photo |
| 401 | invalid_request, not_authenticated | Missing, malformed or expired token |
| 402 | insufficient_balance | Not enough credit for the operation |
| 404 | — | Not found, or belongs to another account |
| 409 | job_already_active | An enrichment job is already running for the product |
| 422 | ai_response_parsing_error | Permanent domain error, retrying will not help |
| 429 | — | Rate limit, on enrich and auth scopes |
| 503 | ai_provider_error, research_failed | Temporary provider failure, retry later |
404 versus 403
Another account's object returns 404, not 403. This is intentional: a 403 would confirm that the object exists.
Retrying
Only 503 and rate limits are worth an automatic retry. 422 is permanent — the job must be corrected, or retried through POST /jobs/{job_id}/retry/ after a fix, not by repeating the same request.
Known deviation
One path does not follow the envelope: PUT /products/{id}/content/ for a product that has no content yet returns 400 with a bare JSON list of strings, not the object above. A client that assumes the envelope everywhere will fail on that path. Treat PUT .../content/ as a special case until the backend is fixed.