> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fetchin.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Handle Fetchin API errors with a single response shape and stable machine-readable codes

## Error Response Format

Every Fetchin API error returns the **same JSON shape**: a human-readable
`error` string and a stable, machine-readable `code`. Branch your integration
on `code` (and the HTTP status) — never on the `error` text, which may change.

```json theme={null}
{
  "error": "Service temporarily unable to serve this request. Please retry.",
  "code": "SERVICE_UNAVAILABLE"
}
```

Some errors include an optional `details` object (or, for the multi-profile
endpoint, top-level fields such as `creditsNeeded`) with extra context.

<Note>
  Authentication errors (`401`, `403`) additionally include legacy `message` and
  `statusCode` fields for backward compatibility. New integrations should rely on
  `error` and `code`.
</Note>

## Error Code Reference

| HTTP | `code`                | Meaning                                    | What to do                                         |
| ---- | --------------------- | ------------------------------------------ | -------------------------------------------------- |
| 400  | `MISSING_PARAMETER`   | A required parameter is absent             | Add the parameter and retry                        |
| 400  | `INVALID_PARAMETER`   | A parameter value is invalid               | Fix the value and retry                            |
| 400  | `INVALID_URN`         | A professional URL/URN could not be parsed | Check the URL/URN format                           |
| 401  | `UNAUTHENTICATED`     | No `X-API-Key` header was sent             | Send your API key                                  |
| 401  | `INVALID_API_KEY`     | The API key is unknown or revoked          | Check your key in the dashboard                    |
| 403  | `FORBIDDEN`           | Access to this resource is not allowed     | —                                                  |
| 404  | `PROFILE_NOT_FOUND`   | The profile does not exist                 | Don't retry; verify the input                      |
| 404  | `POST_NOT_FOUND`      | The post does not exist                    | Don't retry; verify the input                      |
| 404  | `COMPANY_NOT_FOUND`   | The company does not exist                 | Don't retry; verify the input                      |
| 402  | `QUOTA_EXHAUSTED`     | Monthly credit quota exhausted             | Upgrade or wait for renewal                        |
| 429  | `RATE_LIMITED`        | Per-second request rate exceeded           | Back off, honor `Retry-After`                      |
| 500  | `INTERNAL_ERROR`      | An unexpected bug on our side              | Retry with backoff; contact support if it persists |
| 502  | `UPSTREAM_ERROR`      | A genuine upstream service failure         | Retry with backoff                                 |
| 503  | `SERVICE_UNAVAILABLE` | **Our** API is temporarily out of capacity | Retry with backoff, honor `Retry-After`            |
| 504  | `UPSTREAM_TIMEOUT`    | An upstream request timed out              | Retry with backoff                                 |

## HTTP Status Codes

### 400 Bad Request

The request itself is malformed — a missing parameter (`MISSING_PARAMETER`), an
invalid value (`INVALID_PARAMETER`), or an unparseable professional URL/URN
(`INVALID_URN`).

```json theme={null}
{ "error": "Missing profileUrlOrUrn parameter", "code": "MISSING_PARAMETER" }
```

**Solution:** Fix the request. Retrying without changes will fail again.

### 401 Unauthorized

The `X-API-Key` header is missing (`UNAUTHENTICATED`) or invalid
(`INVALID_API_KEY`).

```json theme={null}
{ "error": "Invalid API key", "code": "INVALID_API_KEY" }
```

**Solution:** Include a valid API key from your dashboard.

### 404 Not Found

We reached the service successfully, but the requested profile, post, or company
genuinely does not exist.

```json theme={null}
{ "error": "LinkedIn profile not found", "code": "PROFILE_NOT_FOUND" }
```

**Solution:** Don't retry — verify the URL/URN you sent.

### 402 Payment Required

You ran out of monthly credits (`code: "QUOTA_EXHAUSTED"`). There is no
`Retry-After` — waiting does not help; upgrade your plan or wait for your
renewal date.

```json theme={null}
{ "error": "Quota exceeded. Upgrade your plan or wait for renewal.", "code": "QUOTA_EXHAUSTED" }
```

**Solution:** Upgrade your plan or wait for renewal. Do not retry blindly.

### 429 Too Many Requests

You exceeded your per-second request rate (`code: "RATE_LIMITED"`). A
`Retry-After` header tells you how long to wait.

```json theme={null}
{ "error": "Rate limit exceeded. Try again later.", "code": "RATE_LIMITED" }
```

**Solution:** Back off and retry, honoring `Retry-After`.

### 503 Service Unavailable

**Our** API is temporarily unable to serve your request — we're at capacity or
overloaded. This is on our side. It is **not** your request's fault and **not**
an upstream service outage.

```json theme={null}
{ "error": "Service temporarily unable to serve this request. Please retry.", "code": "SERVICE_UNAVAILABLE" }
```

**Solution:** Retry with exponential backoff, honoring the `Retry-After` header.
The same request will typically succeed shortly after.

### 500 Internal Server Error

An unexpected bug occurred in our API.

```json theme={null}
{ "error": "Internal server error", "code": "INTERNAL_ERROR" }
```

**Solution:** Retry with backoff. If it persists, contact support.

### 502 / 504 Upstream Errors

`UPSTREAM_ERROR` (502) and `UPSTREAM_TIMEOUT` (504) indicate a genuine failure or
timeout on the upstream service's side (as opposed to our capacity, which is `503`). Retry
with backoff.

## Error Handling Examples

### JavaScript/TypeScript

<CodeGroup>
  ```javascript Branch on code theme={null}
  async function fetchProfile(profileUrl, apiKey) {
    const response = await fetch(
      `https://api.fetchin.io/api/v1/profile?profileUrlOrUrn=${encodeURIComponent(profileUrl)}`,
      { headers: { 'X-API-Key': apiKey } },
    );

    if (response.ok) return response.json();

    const { error, code } = await response.json();

    switch (code) {
      case 'MISSING_PARAMETER':
      case 'INVALID_PARAMETER':
      case 'INVALID_URN':
        throw new Error(`Bad request: ${error}`); // fix the request
      case 'INVALID_API_KEY':
      case 'UNAUTHENTICATED':
        throw new Error(`Auth: ${error}`);
      case 'QUOTA_EXHAUSTED':
        throw new Error('Out of credits — upgrade your plan.');
      case 'PROFILE_NOT_FOUND':
      case 'POST_NOT_FOUND':
      case 'COMPANY_NOT_FOUND':
        return null; // genuinely not found — don't retry
      default:
        throw new Error(`${code}: ${error}`);
    }
  }
  ```

  ```javascript Retry on 503/429 with Retry-After theme={null}
  async function fetchWithRetry(url, options, maxRetries = 4) {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      const response = await fetch(url, options);

      // 503 (our capacity) and 429 (rate limit) are safe to retry.
      if (response.status === 503 || response.status === 429) {
        const retryAfter = Number(response.headers.get('Retry-After')) || 2 ** attempt;
        await new Promise((r) => setTimeout(r, retryAfter * 1000));
        continue;
      }

      if (response.status === 500 && attempt < maxRetries - 1) {
        await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
        continue;
      }

      return response;
    }
    throw new Error('Exhausted retries');
  }
  ```
</CodeGroup>

### Python

<CodeGroup>
  ```python Branch on code theme={null}
  import requests

  def fetch_profile(profile_url, api_key):
      resp = requests.get(
          'https://api.fetchin.io/api/v1/profile',
          headers={'X-API-Key': api_key},
          params={'profileUrlOrUrn': profile_url},
      )
      if resp.ok:
          return resp.json()

      body = resp.json()
      code = body.get('code')

      if code in ('MISSING_PARAMETER', 'INVALID_PARAMETER', 'INVALID_URN'):
          raise ValueError(body['error'])          # fix the request
      if code == 'QUOTA_EXHAUSTED':
          raise RuntimeError('Out of credits — upgrade your plan.')
      if code in ('PROFILE_NOT_FOUND', 'POST_NOT_FOUND', 'COMPANY_NOT_FOUND'):
          return None                                # not found — don't retry
      raise RuntimeError(f"{code}: {body['error']}")
  ```

  ```python Retry on 503/429 with Retry-After theme={null}
  import time, requests

  def fetch_with_retry(url, headers, params, max_retries=4):
      for attempt in range(max_retries):
          resp = requests.get(url, headers=headers, params=params)
          # 503 (our capacity) and 429 (rate limit) are safe to retry.
          if resp.status_code in (503, 429):
              retry_after = int(resp.headers.get('Retry-After', 2 ** attempt))
              time.sleep(retry_after)
              continue
          if resp.status_code == 500 and attempt < max_retries - 1:
              time.sleep(2 ** attempt)
              continue
          return resp
      raise RuntimeError('Exhausted retries')
  ```
</CodeGroup>

## Best Practices

<AccordionGroup>
  <Accordion title="Branch on `code`, not the message">
    The `error` string is for humans and may change. The `code` is a stable
    contract — switch on it to decide how to react.
  </Accordion>

  <Accordion title="Retry 503, 500, and 429 — but not 4xx">
    `503`, `500`, `429`, `502`, and `504` are transient and safe to retry with
    exponential backoff. A `400`/`401`/`404` will keep failing until you change
    the request.
  </Accordion>

  <Accordion title="Honor Retry-After">
    `429` and `503` responses include a `Retry-After` header (seconds). Wait at
    least that long before retrying.
  </Accordion>

  <Accordion title="Distinguish RATE_LIMITED from QUOTA_EXHAUSTED">
    They now have different HTTP statuses: `QUOTA_EXHAUSTED` is `402` (you need
    more credits — upgrade or wait for renewal), `RATE_LIMITED` is `429` (slow
    down and honor `Retry-After`). The `code` field carries the same
    distinction. They require different handling.
  </Accordion>

  <Accordion title="Treat 503 as 'try again', not 'no data'">
    `503 SERVICE_UNAVAILABLE` means we couldn't serve you right now — it does not
    mean the profile/post is empty or gone. Retry shortly.
  </Accordion>
</AccordionGroup>

## Quota & Credits

Failed requests (any `4xx`/`5xx`) do **not** consume credits. See
[Quotas](/concepts/quotas) and [Rate Limits](/concepts/rate-limits) for details.
