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

# Errors

> Every error the Offergrid API returns, what causes it, and how to recover

Offergrid uses conventional HTTP status codes. A `2xx` means the request
succeeded, a `4xx` means something about the request needs fixing, and a `5xx`
means the failure was ours.

## Error format

Almost every error returns the same envelope:

```json theme={null}
{
  "statusCode": 404,
  "message": "Offer not found",
  "error": "Not Found"
}
```

| Field        | Type                | Description                                                                                          |
| ------------ | ------------------- | ---------------------------------------------------------------------------------------------------- |
| `statusCode` | integer             | The HTTP status, repeated in the body.                                                               |
| `message`    | string \| string\[] | What went wrong. An **array** when request-body validation failed — one entry per failed constraint. |
| `error`      | string              | Short, stable name for the status code.                                                              |

<Warning>
  `message` is not always a string. Request-body validation failures return an
  array, and two endpoints return a richer shape (see
  [Validation errors](#validation-errors) below). Normalize before displaying:
  `Array.isArray(body.message) ? body.message.join('; ') : body.message`.
</Warning>

## Status codes

| Code  | Meaning                                                                             | What to do                                                                 |
| ----- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `400` | The request body failed validation.                                                 | Fix the request. Do not retry unchanged.                                   |
| `401` | Missing, malformed, or revoked API key.                                             | Check the `x-api-key` header. Do not retry unchanged.                      |
| `403` | Valid key, wrong role for this endpoint.                                            | Use an endpoint your team's role allows, or have your team's role updated. |
| `404` | The resource does not exist, or is not yours.                                       | Check the id. Note that another team's resource returns 404, not 403.      |
| `409` | The request conflicts with existing state — usually a duplicate SKU or brand name.  | Change the conflicting value, or update the existing record instead.       |
| `429` | Rate limited. Only reachable on the [public endpoints](/docs/api-reference/rate-limits). | Back off and retry.                                                        |
| `500` | Something failed on our side.                                                       | Retry with exponential backoff. Safe for idempotent requests.              |

<Note>
  **404 is used for authorization failures on resources.** Every provider and
  reseller endpoint is scoped to your team, so requesting an offer or order
  belonging to another team returns `404 Not Found` rather than `403 Forbidden`.
  This is deliberate — a 403 would confirm the resource exists.

  `403` means something different: your API key is valid but your *team role*
  does not grant access to that whole class of endpoint — a reseller team
  calling `/provider/*`, or vice versa.
</Note>

## Validation errors

### Request-body validation

Every endpoint that accepts a body validates it before any work happens.
Unknown properties are rejected rather than ignored, so a typo in a field name
is a `400`, not a silently dropped value.

```json theme={null}
{
  "statusCode": 400,
  "message": [
    "name should not be empty",
    "category must be one of the following values: internet, electricity",
    "property monthlyPrce should not exist"
  ],
  "error": "Bad Request"
}
```

Each entry names the offending field first, so they can be mapped back to form
fields by prefix.

### Publish validation

[`POST /provider/offers/{id}/publish`](/docs/provider-api-reference/provider-offers/publish-an-offer)
checks an offer against the publish-readiness rules, which are richer than
field-level validation. Its `400` carries a different, structured shape:

```json theme={null}
{
  "error": "Validation failed",
  "message": "Please fix 2 validation issues before publishing.",
  "validationErrors": [
    {
      "section": "pricing",
      "field": "monthlyPrice",
      "message": "Monthly price is required"
    },
    {
      "section": "internetDetails",
      "field": "downloadSpeedMbps",
      "message": "Download speed is required for internet offers"
    }
  ],
  "sections": {}
}
```

| Field                        | Description                                                         |
| ---------------------------- | ------------------------------------------------------------------- |
| `validationErrors`           | Flat list — one entry per unmet publish rule.                       |
| `validationErrors[].section` | The editor section the field belongs to.                            |
| `validationErrors[].field`   | Dot-delimited path to the field.                                    |
| `sections`                   | The same errors grouped by section, for rendering inline in a form. |

<Note>
  This response has no `statusCode` field — the status is on the HTTP response
  only. Branch on the HTTP status, not on the presence of `statusCode` in the
  body.
</Note>

The CSV bulk endpoints (`/provider/offers/bulk-upload`, `/bulk-update`, and
their `/validate` variants) report per-row problems in their `200`/`201` body
rather than as an error — a partially-valid file is a successful request with a
results breakdown, not a failure.

## Handling errors

Retry only what is retryable. A `400`, `401`, `403`, `404`, or `409` will
return the same result no matter how many times you send it; retrying wastes
your budget and ours.

```typescript theme={null}
const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function callOffergrid(path: string, init: RequestInit = {}, maxAttempts = 4) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`https://api.offergrid.io${path}`, {
      ...init,
      headers: { ...init.headers, 'x-api-key': process.env.OFFERGRID_API_KEY! },
    });

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

    if (!RETRYABLE.has(response.status) || attempt === maxAttempts) {
      const body = await response.json().catch(() => ({}));
      const detail = Array.isArray(body.message) ? body.message.join('; ') : body.message;
      throw new Error(`Offergrid ${response.status}: ${detail ?? response.statusText}`);
    }

    // Honor Retry-After when present, otherwise exponential backoff with jitter.
    const retryAfter = Number(response.headers.get('retry-after'));
    const delayMs = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : 2 ** attempt * 250 + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
}
```

<Warning>
  **Non-idempotent writes need care.** `POST /reseller/orders` creates an order;
  a blind retry after a timeout can create a second one. If a write times out
  without a response, reconcile with
  [`GET /reseller/orders`](/docs/reseller-api-reference/reseller-orders/list-your-orders)
  before retrying rather than sending it again.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="Rate limits" icon="gauge-high" href="/docs/api-reference/rate-limits">
    What is limited, and what is not
  </Card>

  <Card title="API conventions" icon="list-check" href="/docs/api-reference/conventions">
    List responses, identifiers, timestamps, and money
  </Card>

  <Card title="Authentication" icon="key" href="/docs/api-reference/authentication">
    Keys, headers, roles, and rotation
  </Card>

  <Card title="Versioning" icon="code-branch" href="/docs/api-reference/versioning">
    How the API changes, and what we promise not to break
  </Card>
</CardGroup>
