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

# Handle errors and retries

> What each status means on this API, which responses to retry and which never to, and a retry wrapper that reads error_code and Retry-After instead of guessing.

Every error is an RFC 9457 problem document with a stable `error_code` and, when there is a self-service fix, a `resolve` object that says who fixes it and where. Branch on `error_code`, not on the status alone: two 402s can mean "add a credential" and "stop, the plan needs attention", and only one of them is worth retrying.

## The envelope

```json theme={"dark"}
{
  "type": "https://docs.particle.pro/errors/validation_error",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "The q parameter is not supported on this endpoint. To search for a person, company, or knowledge graph entity by name, nickname, stock ticker, @handle, or domain, use GET /v1/entities/search (e.g. /v1/entities/search?q=sam+altman). This endpoint lists the most-mentioned entities and filters by type, podcast_id, or ids.",
  "error_code": "validation_error"
}
```

That is a real response to `GET /v1/entities?q=altman`. `detail` names the endpoint to use and gives a worked example; a 422 from a bad parameter names the parameter in `errors[]`. Read it once, change the request, and send it once.

## What to do by status

| Status   | `error_code`                                                                                                 | What it means                                                               | What to do                                                                                                                                                 |
| -------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400, 422 | `bad_request`, `validation_error`, `unresolved_reference`                                                    | The request is malformed or names something that does not resolve           | Read `detail` and `errors[]`, fix the request, send it once. Never resend it unchanged.                                                                    |
| 401      | `api_key_required`                                                                                           | No usable key: missing, mistyped, revoked, or expired all get this response | Check the header (`X-API-Key`) and the key at platform.particle.pro/tokens.                                                                                |
| 402      | `payment_required`                                                                                           | The request carried no credential                                           | Add a key, or pay per request with [x402](/x402).                                                                                                          |
| 402      | `no_active_plan`, `spend_limit_exceeded`, `credits_depleted`, `payment_delinquent`, `subscription_suspended` | The organization's billing state blocks the call                            | Stop the loop. `resolve.action` and `resolve.url` say what fixes it and who can; retrying cannot change it.                                                |
| 403      | `enterprise_required`, `not_a_member`, `forbidden`                                                           | The credential is valid but not entitled                                    | Check the plan or the project the credential is scoped to; an OAuth grant for the wrong project is the usual cause.                                        |
| 404      | `not_found`                                                                                                  | The id does not resolve                                                     | If it is a slug you built, resolve the name instead; if it is a podcast slug on an episode endpoint, list the show's episodes first. Do not try spellings. |
| 429      | `rate_limit_exceeded`                                                                                        | Over 10,000 requests per minute for the organization                        | Wait `Retry-After` seconds, then continue.                                                                                                                 |
| 5xx      | `internal_error`                                                                                             | Something failed on our side                                                | Back off exponentially with jitter and retry a bounded number of times; quote `X-Trace-ID` if it persists.                                                 |

Two 402 codes deserve emphasis because they are the ones clients retry into the ground: `spend_limit_exceeded` and `credits_depleted` do not clear on their own, and every retry is another rejected request. Surface `resolve.message` to the person who owns the account and stop.

## A retry wrapper

Retry only where the table says to, and let the server pace you.

<CodeGroup>
  ```js JavaScript theme={"dark"}
  async function particle(url, { attempts = 5 } = {}) {
    if (!Number.isInteger(attempts) || attempts < 1) throw new RangeError("attempts must be a positive integer");
    for (let attempt = 1; ; attempt++) {
      const res = await fetch(url, { headers: { "X-API-Key": process.env.PARTICLE_API_KEY } });
      if (res.ok) return res.json();

      const problem = await res.json().catch(() => ({}));
      const retryable = res.status === 429 || res.status >= 500;
      if (!retryable || attempt >= attempts) {
        throw new Error(`${res.status} ${problem.error_code ?? ""}: ${problem.detail ?? ""}`);
      }
      const retryAfter = Number(res.headers.get("Retry-After"));
      const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(30_000, 500 * 2 ** attempt) * (0.5 + Math.random());
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  ```

  ```python Python theme={"dark"}
  import os, random, time, httpx

  def particle(url, params=None, attempts=5):
      if attempts < 1:
          raise ValueError("attempts must be at least 1")
      headers = {"X-API-Key": os.environ["PARTICLE_API_KEY"]}
      attempt = 0
      while True:
          attempt += 1
          res = httpx.get(url, params=params, headers=headers)
          if res.is_success:
              return res.json()
          problem = res.json() if res.headers.get("content-type", "").startswith("application/problem+json") else {}
          retryable = res.status_code == 429 or res.status_code >= 500
          if not retryable or attempt >= attempts:
              raise RuntimeError(f"{res.status_code} {problem.get('error_code', '')}: {problem.get('detail', '')}")
          retry_after = float(res.headers.get("Retry-After", 0) or 0)
          delay = retry_after if retry_after > 0 else min(30, 0.5 * 2 ** attempt) * (0.5 + random.random())
          time.sleep(delay)
  ```
</CodeGroup>

The wrapper never retries a 4xx other than 429, so a billing state, a bad parameter, or a constructed slug surfaces as one clear exception instead of a burst of identical requests.

## Parameters that cause avoidable errors

* **Omit optional parameters you do not need.** A placeholder such as `x`, `-`, or `__omit__` is a real filter value; it fails validation or matches nothing.
* **One name per concept.** `q` is the free-text query wherever one exists (`/v1/podcasts/search`, `/v1/companies`, `/v1/entities/search`); `/v1/entities` lists and filters and takes no `q`. Sending `q`, `query`, and `search` together does not hedge; it trips validation.
* **Slugs come from responses.** A constructed slug returns 404; resolve the name and take the slug from the result. See [From an empty page to the data](/recipes/empty-results).

## Related

* [Errors overview](/errors/overview) for the full code catalog and the `resolve` contract
* [Concepts](/concepts) for authentication and rate limits
* [Pay per request with x402](/x402) for the keyless 402 flow
