# Errors and retries

> Error format, every status code and message, and when to retry a HireLayer API call.

Source: https://onlineresumeparser.com/api-docs/errors · Markdown: https://onlineresumeparser.com/api-docs/errors.md

Successful calls return `2xx`. Every error returns a non-2xx status and a JSON body with an `error` string. Branch on the HTTP status first.

**Gateway errors (authentication, credits, CV Extract)**

```json
{
  "error": "This document does not appear to be a CV or resume. Please upload a CV or resume and try again.",
  "code": "DOCUMENT_NOT_A_RESUME"
}
```

**Service errors (Job Extract, Match, Rank, Skills)**

```json
{
  "success": false,
  "error": "job_text cannot be empty"
}
```

- `error` is always present and human-readable. Every exact message is listed in the catalogue below.
- `code` is present on CV Extract errors: `INVALID_FILE`, `DOCUMENT_NOT_A_RESUME`, `DOCUMENT_TEXT_EMPTY`, `DOCUMENT_UNREADABLE`, `DOCUMENT_TOO_LARGE`, `PARSER_UNAVAILABLE`.
- CV Extract errors also carry an `x-parser-request-id` header. Log it.

## Status codes

| Status | Meaning | Action |
| --- | --- | --- |
| `400` | Invalid request: missing field, wrong type, limit exceeded. | Fix the request. Do not retry. |
| `401` | Missing or invalid API key. | Fix the key. Do not retry. |
| `403` | No credit left. | Add credits or upgrade, then retry. |
| `404` | Unknown path or wrong method. | Check the endpoint. |
| `413` | Upload too large (CV Extract). | Send a smaller file. |
| `415` | Wrong `Content-Type` (CV Extract). | Send `multipart/form-data`. |
| `422` | The document cannot be parsed as a resume (CV Extract). | Do not retry the same file. |
| `500` | Invalid JSON body, or an unexpected failure. | Check the body, then retry with backoff. |
| `502` `503` `504` | Temporary failure or timeout. | Retry with backoff; honour `Retry-After`. |

## Retry policy

- Retry `5xx` responses and connection failures, at most 3 times, with exponential backoff and jitter (about 1 s, 2 s, 4 s).
- When a `Retry-After` header is present, wait that many seconds.
- Never retry `4xx` unchanged. `403` can succeed once credits are added.
- Failed calls consume no credit. There is no idempotency key: a call your client abandons can still complete and be charged, so keep client timeouts above the gateway timeouts (150 s for CV Extract, 65 s otherwise).

**Reusable client**

**Python**

```python
# hirelayer.py — minimal client with retries. Requires: pip install requests
import mimetypes
import os
import random
import time

import requests

API_BASE = "https://onlineresumeparser.com/api"


class HireLayerError(Exception):
    def __init__(self, status, message, code=None, request_id=None):
        super().__init__(f"HireLayer {status}: {message}")
        self.status = status
        self.code = code
        self.request_id = request_id


def _delay(attempt, retry_after):
    if retry_after and retry_after.isdigit():
        return int(retry_after)
    return min(30, 2**attempt) + random.random()


def call(method, path, *, json=None, files=None, data=None, timeout=65, max_retries=3):
    """Call HireLayer. Retries 5xx and connection failures, raises HireLayerError otherwise."""
    headers = {"X-API-Key": os.environ["HIRELAYER_API_KEY"]}
    for attempt in range(max_retries + 1):
        try:
            response = requests.request(
                method,
                API_BASE + path,
                headers=headers,
                json=json,
                files=files,
                data=data,
                timeout=timeout,
            )
        except requests.ConnectionError:
            if attempt == max_retries:
                raise
            time.sleep(_delay(attempt, None))
            continue

        if response.ok:
            return response.json()
        if response.status_code >= 500 and attempt < max_retries:
            time.sleep(_delay(attempt, response.headers.get("Retry-After")))
            continue

        try:
            body = response.json()
        except ValueError:
            body = {}
        raise HireLayerError(
            response.status_code,
            body.get("error", response.reason),
            body.get("code"),
            response.headers.get("x-parser-request-id"),
        )


def parse_resume(path, application_id=None):
    content_type = mimetypes.guess_type(path)[0] or "application/octet-stream"
    with open(path, "rb") as file:
        content = file.read()  # bytes can be re-sent on retry
    return call(
        "POST",
        "/v3/parser",
        files={"file": (os.path.basename(path), content, content_type)},
        data={"application_id": application_id} if application_id else None,
        timeout=150,
    )

```

**TypeScript**

```typescript
// hirelayer.mts — minimal client with retries. Node.js 18+, run with tsx.
const API_BASE = 'https://onlineresumeparser.com/api'

export class HireLayerError extends Error {
  status: number
  code?: string
  requestId?: string

  constructor(status: number, message: string, code?: string, requestId?: string) {
    super(`HireLayer ${status}: ${message}`)
    this.status = status
    this.code = code
    this.requestId = requestId
  }
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

function retryDelay(attempt: number, retryAfter: string | null) {
  const seconds = Number(retryAfter)
  if (retryAfter && Number.isInteger(seconds)) return seconds * 1000
  return Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 1000
}

/** Calls HireLayer. Retries 5xx responses, throws HireLayerError otherwise. */
export async function callHireLayer<T>(
  path: string,
  options: { json?: unknown; form?: FormData; timeoutMs?: number } = {},
  maxRetries = 3
): Promise<T> {
  const headers: Record<string, string> = {
    'X-API-Key': process.env.HIRELAYER_API_KEY!,
  }
  let body: string | FormData | undefined = options.form
  if (options.json !== undefined) {
    headers['Content-Type'] = 'application/json'
    body = JSON.stringify(options.json)
  }

  for (let attempt = 0; ; attempt++) {
    const response = await fetch(API_BASE + path, {
      method: body === undefined ? 'GET' : 'POST',
      headers,
      body,
      signal: AbortSignal.timeout(options.timeoutMs ?? 65_000),
    })
    if (response.ok) return (await response.json()) as T
    if (response.status >= 500 && attempt < maxRetries) {
      await sleep(retryDelay(attempt, response.headers.get('Retry-After')))
      continue
    }
    const payload = await response.json().catch(() => ({}))
    throw new HireLayerError(
      response.status,
      payload.error ?? response.statusText,
      payload.code,
      response.headers.get('x-parser-request-id') ?? undefined
    )
  }
}

```

## Error catalogue

Exact `error` messages, per endpoint.

### `POST /api/v3/parser`

| Status | `error` | `code` | Retry |
| --- | --- | --- | --- |
| `400` | `Missing required file field` | — | Do not retry |
| `400` | `application_id must be a string when provided` | — | Do not retry |
| `400` | `The uploaded file is empty or invalid. Please check the file and try again.` | `INVALID_FILE` | Do not retry |
| `401` | `Missing API Key` | — | Do not retry |
| `401` | `Invalid API Key` | — | Do not retry |
| `403` | `Insufficient credits available` | — | Fix, then retry |
| `413` | `The uploaded file is too large to process. Please upload a smaller file.` | — | Do not retry |
| `415` | `Content-Type must be multipart/form-data` | — | Do not retry |
| `422` | `This document does not appear to be a CV or resume. Please upload a CV or resume and try again.` | `DOCUMENT_NOT_A_RESUME` | Do not retry |
| `422` | `The text could not be extracted from this document. Please verify that the file is readable and contains selectable text.` | `DOCUMENT_TEXT_EMPTY` | Do not retry |
| `422` | `The document could not be read. Please upload a valid, readable file.` | `DOCUMENT_UNREADABLE` | Do not retry |
| `422` | `This document contains too much text to process. Please try a shorter or simpler version.` | `DOCUMENT_TOO_LARGE` | Do not retry |
| `500` | `Internal Server Error` | — | Retry with backoff |
| `502` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | Retry with backoff |
| `503` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | Retry with backoff |
| `503` | `Credit service temporarily unavailable` | — | Retry with backoff |
| `504` | `An error occurred while processing the document or extracting its text. Please try again later.` | `PARSER_UNAVAILABLE` | Retry with backoff |

### `POST /api/v1/jobs/extract-criteria`

| Status | `error` | `code` | Retry |
| --- | --- | --- | --- |
| `400` | `Request body must be a JSON object` | — | Do not retry |
| `400` | `The request contains unsupported fields` | — | Do not retry |
| `400` | `The 'job_text' field is required` | — | Do not retry |
| `400` | `job_text cannot be empty` | — | Do not retry |
| `400` | `job_text must be 50000 characters or less` | — | Do not retry |
| `401` | `Missing API Key` | — | Do not retry |
| `401` | `Invalid API Key` | — | Do not retry |
| `403` | `Insufficient credits available` | — | Fix, then retry |
| `500` | `Internal server error` | — | Retry with backoff |
| `500` | `Internal server error. Please try again later.` | — | Retry with backoff |
| `502` | `Upstream API unavailable` | — | Retry with backoff |
| `502` | `The AI processing step failed. Please try again later.` | — | Retry with backoff |
| `503` | `Credit service temporarily unavailable` | — | Retry with backoff |

### `POST /api/v1/matching/job-candidate`

| Status | `error` | `code` | Retry |
| --- | --- | --- | --- |
| `400` | `Request body must be a JSON object` | — | Do not retry |
| `400` | `The request contains unsupported fields` | — | Do not retry |
| `400` | `The 'job_text' field is required` | — | Do not retry |
| `400` | `job_text cannot be empty` | — | Do not retry |
| `400` | `job_text must be 50000 characters or less` | — | Do not retry |
| `400` | `The 'candidate_text' field is required` | — | Do not retry |
| `400` | `candidate_text cannot be empty` | — | Do not retry |
| `400` | `candidate_text must be 50000 characters or less` | — | Do not retry |
| `400` | `The 'matching_criteria' field must be an array` | — | Do not retry |
| `400` | `matching_criteria contains an invalid criterion` | — | Do not retry |
| `401` | `Missing API Key` | — | Do not retry |
| `401` | `Invalid API Key` | — | Do not retry |
| `403` | `Insufficient credits available` | — | Fix, then retry |
| `500` | `Internal server error` | — | Retry with backoff |
| `500` | `Internal server error. Please try again later.` | — | Retry with backoff |
| `502` | `Upstream API unavailable` | — | Retry with backoff |
| `502` | `The AI processing step failed. Please try again later.` | — | Retry with backoff |
| `503` | `Credit service temporarily unavailable` | — | Retry with backoff |

### `POST /api/v1/matching/job-candidates/rank`

| Status | `error` | `code` | Retry |
| --- | --- | --- | --- |
| `400` | `Request body must be a JSON object` | — | Do not retry |
| `400` | `The request contains unsupported fields` | — | Do not retry |
| `400` | `The 'job_text' field is required` | — | Do not retry |
| `400` | `job_text cannot be empty` | — | Do not retry |
| `400` | `job_text must be 50000 characters or less` | — | Do not retry |
| `400` | `The 'candidates' field must be an array` | — | Do not retry |
| `400` | `candidates cannot be empty` | — | Do not retry |
| `400` | `candidates must contain 10 candidates or fewer` | — | Do not retry |
| `400` | `candidates contains an invalid candidate` | — | Do not retry |
| `400` | `candidate_text must be 50000 characters or less` | — | Do not retry |
| `400` | `candidates contains duplicate ids` | — | Do not retry |
| `401` | `Missing API Key` | — | Do not retry |
| `401` | `Invalid API Key` | — | Do not retry |
| `403` | `Insufficient credits available` | — | Fix, then retry |
| `500` | `Internal server error` | — | Retry with backoff |
| `500` | `Internal server error. Please try again later.` | — | Retry with backoff |
| `502` | `Upstream API unavailable` | — | Retry with backoff |
| `502` | `The AI processing step failed. Please try again later.` | — | Retry with backoff |
| `503` | `Credit service temporarily unavailable` | — | Retry with backoff |

### `POST /api/v1/skills/match`

| Status | `error` | `code` | Retry |
| --- | --- | --- | --- |
| `400` | `Provide either 'skill' or 'skills', not both` | — | Do not retry |
| `400` | `The 'skill' or 'skills' field is required` | — | Do not retry |
| `400` | `The 'skill' field is required` | — | Do not retry |
| `400` | `Skill cannot be empty` | — | Do not retry |
| `400` | `The 'skills' (list) field is required` | — | Do not retry |
| `400` | `The 'skills' field must be a non-empty list` | — | Do not retry |
| `400` | `Maximum 100 skills per batch request` | — | Do not retry |
| `400` | `top_k must be an integer between 1 and 50` | — | Do not retry |
| `400` | `Request body must be a JSON object` | — | Do not retry |
| `401` | `Missing API Key` | — | Do not retry |
| `401` | `Invalid API Key` | — | Do not retry |
| `403` | `Insufficient credits available` | — | Fix, then retry |
| `500` | `Internal server error` | — | Retry with backoff |
| `500` | `Internal server error. Please try again later.` | — | Retry with backoff |
| `502` | `Upstream API unavailable` | — | Retry with backoff |
| `503` | `Credit service temporarily unavailable` | — | Retry with backoff |

### `GET /api/v1/skills`

| Status | `error` | `code` | Retry |
| --- | --- | --- | --- |
| `401` | `Missing API Key` | — | Do not retry |
| `401` | `Invalid API Key` | — | Do not retry |
| `403` | `Insufficient credits available` | — | Fix, then retry |
| `500` | `Internal server error` | — | Retry with backoff |
| `502` | `Upstream API unavailable` | — | Retry with backoff |
| `503` | `Credit service temporarily unavailable` | — | Retry with backoff |

### `GET /api/v1/health`

| Status | `error` | `code` | Retry |
| --- | --- | --- | --- |
| `502` | `Upstream API unavailable` | — | Retry with backoff |

### `POST /api/v2/parser`

| Status | `error` | `code` | Retry |
| --- | --- | --- | --- |
| `400` | `An error occurred while processing the document or extracting its text. Please try again later.` | — | Do not retry |
| `401` | `Missing API Key` | — | Do not retry |
| `401` | `Invalid API Key` | — | Do not retry |
| `403` | `Insufficient credits available` | — | Fix, then retry |
| `415` | `Content-Type must be multipart/form-data` | — | Do not retry |
| `503` | `Credit service temporarily unavailable` | — | Retry with backoff |

## Next

- [Limits and credits](https://onlineresumeparser.com/api-docs/limits.md): How credits are consumed, request size and batch limits, timeouts and concurrency guidance for the HireLayer APIs.
- [Build a screening pipeline](https://onlineresumeparser.com/api-docs/screening-pipeline.md): Chain HireLayer CV Extract, Job Extract, Rank and Match to parse resumes, score them against a job and explain the result.
