Concepts
Error Handling
Every API error returns a consistent JSON structure with a machine-readable code, human-readable message, and optional details. Learn the full error taxonomy and how to handle each category.
Error Response Format
All error responses use a consistent envelope. The top-level error object contains three fields:
code — A unique, machine-readable string you can switch on programmatically (e.g. INSUFFICIENT_CREDITS).
message — A human-readable explanation safe to log or display.
details — An optional object with structured context such as missing fields, invalid values, or retry timing.
// Route error (most endpoints)
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Not enough credits to submit this application"
}
}
// Validation error (422)
{
"error": {
"code": "VALIDATION_ERROR",
"message": "field 'email' is required",
"details": [
{ "loc": ["body", "candidate", "personalInformation", "email"], "msg": "field required", "type": "value_error.missing" }
]
}
}
// Rate limit error (429) — includes retryAfter
{
"error": {
"code": "RATE_LIMIT_CONCURRENT_EXCEEDED",
"message": "Too many concurrent requests. Maximum 25 allowed.",
"retryAfter": 30
}
}HTTP Status Codes
The API uses standard HTTP status codes.
Authentication Errors
These errors occur when the request cannot be authenticated. Check your API key or admin secret.
| Name | Type | Presence | Description |
|---|---|---|---|
INVALID_API_KEY | 401 | Always | The Bearer token is missing, malformed, or does not match any active client. Verify your API key starts with bp_live_ and is 40 characters. |
UNAUTHORIZED | 401 | Always | The X-Admin-Secret header is missing or does not match the server's configured secret. Only applies to /internal/ endpoints. |
CLIENT_INACTIVE | 403 | Always | The API key is valid but the client account has been suspended. Contact support or use PUT /internal/clients/:clientId to reactivate. |
ADMIN_NOT_CONFIGURED | 500 | Always | The server's admin secret is not configured. This is a server-side configuration issue — contact the platform administrator. |
Validation Errors
Returned when request parameters or body fields fail validation. The details array in the response pinpoints every invalid field with its location, message, and error type.
| Name | Type | Presence | Description |
|---|---|---|---|
VALIDATION_ERROR | 422 | Always | One or more request body fields failed schema validation. Check the details array for specific fields and error messages. |
INVALID_STATUS | 400 | Always | An invalid status value was provided. For users and clients, status must be 'active' or 'inactive'. For applications, see the valid status list in the error message. |
INVALID_MANDATORY_FIELDS | 400 | Always | Attempted to remove a system-required field from the mandatory fields configuration. The error message lists which fields cannot be removed. |
INVALID_IDEMPOTENCY_KEY | 400 | Always | The Idempotency-Key header is not a valid UUID v4. Generate keys using a proper UUID library. |
{
"error": {
"code": "VALIDATION_ERROR",
"message": "field 'email' is required",
"details": [
{
"loc": ["body", "personalInformation", "email"],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": ["body", "resumeUrl"],
"msg": "invalid url format",
"type": "value_error.url"
}
]
}
}Resource Errors
Returned when a referenced resource does not exist or belongs to a different client. The API enforces ownership — you can only access resources that belong to your authenticated client.
| Name | Type | Presence | Description |
|---|---|---|---|
USER_NOT_FOUND | 404 | Always | No user exists with the given ID, or the user belongs to a different client. |
PROFILE_NOT_FOUND | 404 | Always | No candidate profile exists with the given ID, or it belongs to a different client. |
PROFILE_INCOMPLETE | 400 | Always | The candidate profile is missing mandatory fields required for application submission. Validate the profile first with GET /profiles/:profileId/validate. |
SESSION_NOT_FOUND | 404 | Always | No session exists with the given ID, or it belongs to a different client. |
RESUME_NOT_FOUND | 404 | Always | No resume exists with the given ID, or it belongs to a different client. |
APPLICATION_NOT_FOUND | 404 | Always | No application exists with the given ID, or it belongs to a different client. |
CLIENT_NOT_FOUND | 404 | Always | No client exists with the given ID. Only returned by /internal/ admin endpoints. |
NOT_FOUND | 404 | Always | Generic not-found for search records, demo users, or demo profiles. |
Business Logic Errors
These errors indicate the request is valid but violates a business rule — duplicate resources, insufficient credits, or invalid session operations.
| Name | Type | Presence | Description |
|---|---|---|---|
INSUFFICIENT_CREDITS | 402 | Always | Your account has zero credits remaining. Applications cannot be submitted until credits are added. |
DUPLICATE_APPLICATION | 409 | Always | An application with the same candidate email and job URL already exists. Each candidate can only apply to a job once. |
DUPLICATE_USER | 409 | Always | A user (or demo user) with the same externalUserId already exists for your client. |
INVALID_SESSION_TYPE | 400 | Always | The operation is not valid for this session type. For example, PUT /sessions/:id/recurring only works on autopilot_recurring sessions. |
INVALID_SESSION_STATE | 400 | Always | The session is in a state that does not allow this operation. For example, you cannot pause a completed session or update a cancelled session. |
RATE_LIMIT_CONCURRENT_EXCEEDED | 429 | Always | Too many concurrent requests. The response includes retryAfter (seconds) and the X-RateLimit-Limit / X-RateLimit-Remaining headers. |
Handling Errors in Code
Build a centralized error handler that inspects the HTTP status code first, then the error code for specific logic. Here is a production-ready pattern for each language.
# Use -w to capture the HTTP status code alongside the response body
response=$(curl -s -w "\n%{http_code}" -X POST https://apply-api.boringproject.ai/api/v1/apply \
-H "Authorization: Bearer bp_live_..." \
-H "Content-Type: application/json" \
-d '{"job_url": "https://boards.greenhouse.io/acme/jobs/12345", "candidate": {...}, "resume_url": "..."}')
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" -ge 400 ]; then
error_code=$(echo "$body" | jq -r '.detail.code // .error.code')
error_msg=$(echo "$body" | jq -r '.detail.message // .error.message')
echo "Error [$http_code] $error_code: $error_msg"
case "$error_code" in
INSUFFICIENT_CREDITS) echo "Add credits before retrying" ;;
DUPLICATE_APPLICATION) echo "Already applied — skip" ;;
RATE_LIMIT_CONCURRENT_EXCEEDED) echo "Waiting 30s..."; sleep 30 ;;
VALIDATION_ERROR) echo "$body" | jq '.error.details' ;;
esac
fiRetry Strategy
Not all errors should be retried. Follow these guidelines:
Do not retry 400, 401, 402, 403, 404, 409, or 422 errors — these indicate a problem with the request itself. Fix the input and try again.
Retry 429 errors after waiting the number of seconds specified in the retryAfter field (default 30 seconds).
Retry 500, 502, 503, and 504 errors with exponential backoff: wait 1s, 2s, 4s, 8s, up to a maximum of 60 seconds. Add random jitter (0-1s) to prevent thundering herds.
Set a maximum retry count (recommended: 3 attempts) to avoid infinite loops.
Always use idempotency keys (Idempotency-Key header) on POST requests so that retries do not create duplicate resources.
async function withRetry(fn, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (!(err instanceof BoringApiError)) throw err;
// Never retry client errors (except 429)
if (err.status >= 400 && err.status < 500 && err.status !== 429) throw err;
if (attempt === maxRetries) throw err;
const delay = err.code === 'RATE_LIMIT_CONCURRENT_EXCEEDED'
? 30_000
: Math.min(1000 * 2 ** attempt, 60_000) + Math.random() * 1000;
console.log(`Retry ${attempt + 1}/${maxRetries} in ${Math.round(delay / 1000)}s...`);
await new Promise(r => setTimeout(r, delay));
}
}
}Related docs
Continue reading
Rate Limiting
Understand API rate limits, response headers, and how to handle 429 responses gracefully with exponential backoff.
Authentication
Authenticate requests to the Boring Project API using Bearer tokens with your API key.
Idempotency
Prevent duplicate operations by including an Idempotency-Key header in POST requests.