Errors
Failures arrive in one of two shapes depending on how far into the request they happened. Check the status code before you parse the body.
Two shapes
Rejections before the handler runs — a missing or invalid key, a workspace this key cannot reach, an insufficient permission level, a rate limit — come back as plain text. The body is the message and nothing else.
HTTP/1.1 401 Unauthorized
content-type: text/plain;charset=UTF-8
Unauthorized. Missing API key.Rejections from inside the handler — a resource that does not exist, a malformed body, a balance that cannot cover the work — come back as JSON, in the envelope's failure form.
HTTP/1.1 404 Not Found
content-type: application/json
{
"success": false,
"error": "Dataset not found"
}There is no unifying error object and no machine-readable error code. Branch on the status; use the message for a log line, not for control flow — the wording is not contract and can be improved without notice.
res=$(curl -s -w '\n%{http_code}' \
https://api.ragextract.com/v2/workspaces/wks_7Kq2mB4nR8vXpL3d/files \
-H "x-api-key: $RAGEXTRACT_API_KEY")
body=${res%$'\n'*}
code=${res##*$'\n'}
# The body is JSON on success and on a handler error, and plain text on an
# auth, permission or rate-limit error. Test the status first.
[ "$code" -ge 400 ] && echo "failed ($code): $body" && exit 1Status codes
| Status | Means |
|---|---|
400 | The request body or query failed validation, or an endpoint's own precondition did not hold — an upload naming neither a file nor a URL, an unknown or expired upload session, an image_url pointing somewhere it may not. |
401 | No key, an unknown key, an expired key, or a share token that does not verify. Every bad-credential path returns the same sentence on purpose, so the response cannot be used to tell a wrong key from an expired one. |
403 | The workspace or table is reachable, but this key's level is below what the endpoint needs — a read-only grant on a write route. |
402 | Not enough credits, or the organisation's monthly spend cap would be passed. From a run it is JSON carrying the numbers (see below); from search it is plain text, because the refusal happens in middleware. |
404 | Either the resource does not exist, or it exists and this key cannot reach it at all. The two are deliberately not distinguished — see below. |
429 | Over the rate limit. Back off and retry; there is no header saying how long to wait. |
503 | A dependency we refuse to serve around — today, the search meter failing to write. Retry: the block claim is idempotent, so a retried search is not charged twice. |
500 | Something on our side. Safe to retry an idempotent read; for a write, check the resource's state before repeating it. |
Why an unreachable workspace is 404
A key that cannot reach a workspace gets 404, not 403, and the same holds for a table. That is not sloppiness. Answering 403 would confirm the id exists, and since ids appear in URLs, a narrowly scoped key could walk the organisation's id space one request at a time and learn what it has.
The rule to build against: 404 means "not yours or not there", and there is no way to tell which from the outside. 403 means "yours, but not at this level" — the fix is a wider scope or a higher sharing level, not a different id.
Running out of credits
Starting a table run when the balance cannot cover it fails with 402 before any work happens, and the JSON body carries the numbers you need to explain it:
cost— what the run would have spent, in credits.balance— what is available.cap— present only when the refusal was the organisation's monthly spend cap rather than the balance. A cap is a self-imposed ceiling, so buying credits does not lift it; raising or clearing it is done in the app.
Price a run first with the free run preview and this never has to be an error path. Note that ingest behaves differently: it refuses only a balance that cannot cover a single page, because the page count is not known until the document has been read.
Silently ignored parameters
Unknown keys in a request body are stripped, not rejected. A misspelled parameter returns 200 with the option dropped, and nothing tells you. If an option appears to do nothing, check its spelling against the endpoint's parameter table before looking anywhere else.