Errors & limits
Error format
Request-level failures (before the stream starts) return an HTTP error status and a JSON body with a consistent shape:
{
"error": {
"type": "authentication_error",
"code": "INVALID_API_KEY",
"message": "Invalid or missing API key."
}
}Status codes
| Status | Code | Meaning |
|---|---|---|
400 | MISSING_QUERY | The query field is missing or empty. |
400 | QUERY_EXCEEDS_MAX_LENGTH | query is longer than 10,000 characters. |
400 | INVALID_REASONING_LEVEL | reasoning_level is not low, medium, or high. |
400 | INVALID_REQUEST | The body is not valid JSON. |
401 | INVALID_API_KEY | Missing, malformed, or revoked credential. |
402 | INSUFFICIENT_CREDITS | Not enough credits for this request. |
429 | RATE_LIMIT_EXCEEDED | Too many concurrent requests for your tier. |
404 | NOT_FOUND | Unknown endpoint. |
502 | PROXY_ERROR | The research backend was unreachable. |
Errors that occur after the stream has started are delivered as a
research.failed event instead — see Streaming & events.
Credits
Each request costs credits based on reasoning_level, which sets the maximum limit of research iterations. The underlying model decides dynamically when it has enough context to answer, meaning it may terminate early and perform fewer iterations than the specified ceiling.
| Reasoning level | Max Iterations | Credits |
|---|---|---|
low | 5 | 10 |
medium | 10 | 15 |
high | 15 | 20 |
New accounts start with 300 free credits per month, renewed on your registration
anniversary date. Credits are deducted before
research begins; if your balance is too low you get 402 INSUFFICIENT_CREDITS
with a Retry-After header.
Concurrency limits
Bahith limits the number of simultaneous in-flight requests per account by tier:
| Tier | Concurrent requests |
|---|---|
| Free | 1 |
| Pro | 3 |
| Scale | 5 |
Exceeding your limit returns 429 RATE_LIMIT_EXCEEDED with a Retry-After
header (seconds). This caps concurrency, not total throughput — retry after the
indicated delay.
Handling 429 and 402
Respect the Retry-After header. For 429, wait the
indicated seconds and retry; for sustained load, queue requests up to your
tier's concurrency limit. For 402, top up credits before
retrying.
research_with_retry() {
local query="$1"
local key="$2"
local max_retries=3
for ((attempt=1; attempt<=max_retries; attempt++)); do
response=$(curl -s -i -N -X POST https://api.bahith.dev/v1/research \
-H "Content-Type: application/json" \
-H "X-API-Key: $key" \
-d "{\"query\": \"$query\"}")
status_code=$(echo "$response" | grep "HTTP/" | awk '{print $2}')
if [ "$status_code" -eq 429 ]; then
retry_after=$(echo "$response" | grep -i "Retry-After" | awk '{print $2}' | tr -d '\r')
retry_after=${retry_after:-5}
echo "Rate limited. Retrying in $retry_after seconds..."
sleep "$retry_after"
continue
fi
echo "$response"
return 0
done
echo "Failed after $max_retries attempts" >&2
return 1
}