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

# Errors

> Standard error response shape, HTTP status codes, and common error codes returned by the v2 API

The v2 REST API uses a single, predictable error envelope across every endpoint. Whatever the failure — a missing auth token, a Zod validation problem, a 404, a downstream rate limit — the body shape is the same. Build your error-handling once and reuse it everywhere.

<Note>
  This page covers v2 REST endpoints. MCP tool errors follow the [ADCP error
  spec](https://adcontextprotocol.org/schemas/3.0.0-rc.3/core/error.json) and
  are returned in `structuredContent` rather than HTTP status codes.
</Note>

## Error envelope

Every non-success response has `data: null` and a populated `error` object:

```json theme={null}
{
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "field": "name",
    "details": {
      "issues": [
        { "path": "name", "message": "Required" }
      ]
    }
  }
}
```

| Field     | Type     | Always present | Description                                                              |
| --------- | -------- | -------------- | ------------------------------------------------------------------------ |
| `code`    | `string` | yes            | Machine-readable error code (see table below)                            |
| `message` | `string` | yes            | Human-readable message safe to surface to end users                      |
| `field`   | `string` | no             | Field path for validation errors (e.g. `start_date`, `targeting.geos`)   |
| `details` | `object` | no             | Structured payload — Zod issues, conflict resource ids, retry hints, etc |

Successful responses have the inverse shape: `{ "data": <result>, "error": null }`. List endpoints add a `meta` block (see [Pagination](/v2/reference/pagination)).

<Note>
  Throughout the Buyer and Storefront task references, the `## Response` examples
  show the **`data` payload only** — the inner result. On the wire it is always
  wrapped in the envelope above: `{ "data": <payload>, "error": null }`, plus a
  `meta` block on list endpoints. Read the result from `response.data`.
</Note>

## HTTP status codes

| Status | Meaning                                                     | Typical `code` values                                                            |
| ------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `400`  | Malformed request                                           | `BAD_REQUEST`, `VALIDATION_ERROR`, `CURRENCY_MISMATCH`                           |
| `401`  | Missing or invalid auth                                     | `UNAUTHORIZED`                                                                   |
| `402`  | Payment required — committed spend would exceed your credit | `CREDIT_LIMIT_EXCEEDED`                                                          |
| `403`  | Authenticated but not allowed                               | `FORBIDDEN`, `ACCESS_DENIED`, `ALPHA_OPT_IN_REQUIRED`, `TOS_ACCEPTANCE_REQUIRED` |
| `404`  | Resource doesn't exist or isn't visible to you              | `NOT_FOUND`                                                                      |
| `409`  | Conflicting state (duplicate, wrong state, budget short)    | `CONFLICT`, `PRICING_NOT_CONFIGURED`, `INSUFFICIENT_MEDIA_BUDGET`                |
| `422`  | Semantically invalid (rare — most things use 400)           | `VALIDATION_ERROR`, `CAPABILITY_NOT_SUPPORTED`                                   |
| `429`  | Rate limit hit                                              | `RATE_LIMITED` (see [Rate Limits](/v2/reference/rate-limits))                    |
| `500`  | Unhandled server error                                      | `INTERNAL_ERROR`                                                                 |
| `501`  | Endpoint exists but not yet implemented                     | `NOT_IMPLEMENTED`                                                                |
| `503`  | Upstream / dependency unavailable                           | `SERVICE_UNAVAILABLE`                                                            |

## Common error codes

| Code                  | When you'll see it                                                            |
| --------------------- | ----------------------------------------------------------------------------- |
| `VALIDATION_ERROR`    | Zod schema rejected the request body, query, or params                        |
| `BAD_REQUEST`         | Generic 400 — usually a malformed param that didn't reach Zod                 |
| `UNAUTHORIZED`        | No bearer token, expired token, or unknown API key                            |
| `FORBIDDEN`           | Auth succeeded but you lack the role/permission                               |
| `ACCESS_DENIED`       | Resource exists but is owned by a different account/advertiser                |
| `NOT_FOUND`           | Resource ID doesn't exist (or is hidden from your scope)                      |
| `CONFLICT`            | Duplicate resource, illegal state transition, or invariant violated           |
| `RATE_LIMITED`        | Too many requests — back off and retry after `Retry-After`                    |
| `INTERNAL_ERROR`      | Unhandled exception — safe to retry once                                      |
| `SERVICE_UNAVAILABLE` | A dependency (downstream agent, billing, signal provider) is temporarily down |

Domain-specific codes you may encounter on campaign endpoints:

| Code                                  | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PRICING_NOT_CONFIGURED`              | Advertiser has no pricing rule for the selected sales agent. Also returned when a campaign budget change involves media buys whose fee terms cannot be determined — `details.unpricedBuyIds` names them; resolve pricing for those buys first                                                                                                                                                                                              |
| `CURRENCY_MISMATCH`                   | Budget currency doesn't match the agent or storefront currency                                                                                                                                                                                                                                                                                                                                                                             |
| `INSUFFICIENT_MEDIA_BUDGET`           | The campaign's `budget.total` can't cover its media buys — e.g. lowering the total below what live buys already allocate. The error names the requested total and the committed allocation                                                                                                                                                                                                                                                 |
| `CREDIT_LIMIT_EXCEEDED`               | Activating or raising a campaign would push your org's committed spend past its available Scope3 credit (`available = funded + creditLimit − committed`). Returned as **HTTP 402**. `details` carries `availableMinor`, `committedMinor`, `fundedMinor`, `creditLimitMinor`, `newCommitmentMinor`, `exceededByMinor`, and `currency` (minor-unit strings). Free up committed budget or add funds, then retry                               |
| `ROUTED_AGENT_REQUIRES_OPERATOR_AUTH` | Deprecated compatibility code for older clients. If received, operator credentials are missing; do not infer a storefront type from the name.                                                                                                                                                                                                                                                                                              |
| `ALPHA_OPT_IN_REQUIRED`               | Feature is in alpha — opt in via support before using                                                                                                                                                                                                                                                                                                                                                                                      |
| `TOS_ACCEPTANCE_REQUIRED`             | Account must accept the latest terms of service before mutating                                                                                                                                                                                                                                                                                                                                                                            |
| `CAPABILITY_NOT_SUPPORTED`            | Campaign update targets a media buy whose downstream sales agent does not declare support for the requested operation. `details.unsupported[]` lists each `{ mediaBuyId, salesAgentId, field, reason }`. The update is rejected upfront with no state changes; fix by removing the unsupported `mediaBuys[]` entries or waiting for the agent to declare support.                                                                          |
| `VERSION_UNSUPPORTED`                 | An AdCP source rejected the protocol version on the request — returned in `structuredContent` on MCP tools, per the ADCP error spec. Correct **only** for a cross-major mismatch (no shared major version). A *same-major* release gap must be downshifted by the source, not errored; seeing this on a same-major minor means the source is on a non-conformant build. See [AdCP versioning & negotiation](/v2/concepts/adcp-versioning). |

## Validation errors

When request validation fails, `code` is `VALIDATION_ERROR` and `details.issues` enumerates every problem Zod found, with dotted field paths:

```json theme={null}
{
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": {
      "issues": [
        { "path": "name", "message": "Required" },
        { "path": "budget.amount", "message": "Number must be positive" },
        { "path": "flightDates.start", "message": "Invalid date" }
      ]
    }
  }
}
```

When a single-field check fails (e.g. a route guard), `field` is set instead:

```json theme={null}
{
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Start date must be in the future",
    "field": "start_date"
  }
}
```

<Tip>
  Always render `details.issues[].path` in your UI — the user usually just needs
  to know which form field to fix.
</Tip>

## Handling errors in client code

<CodeGroup>
  ```bash curl theme={null}
  curl -i https://api.interchange.io/api/v2/buyer/campaigns \
    -H "Authorization: Bearer $SCOPE3_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"advertiserId": 123}'

  # HTTP/1.1 400 Bad Request
  # Content-Type: application/json
  #
  # {
  #   "data": null,
  #   "error": {
  #     "code": "VALIDATION_ERROR",
  #     "message": "Request validation failed",
  #     "details": {
  #       "issues": [
  #         { "path": "name", "message": "Required" },
  #         { "path": "budget", "message": "Required" }
  #       ]
  #     }
  #   }
  # }
  ```

  ```typescript TypeScript theme={null}
  async function createCampaign(body: unknown) {
    const res = await fetch("https://api.interchange.io/api/v2/buyer/campaigns", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SCOPE3_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    const json = await res.json();

    if (!res.ok) {
      const { code, message, field, details } = json.error;

      switch (code) {
        case "VALIDATION_ERROR":
          // Surface details.issues[] in your form UI
          throw new ValidationError(message, details?.issues ?? []);
        case "RATE_LIMITED":
          // Honor Retry-After header
          const retryAfter = Number(res.headers.get("Retry-After") ?? "5");
          throw new RateLimitError(retryAfter);
        case "NOT_FOUND":
        case "ACCESS_DENIED":
          throw new NotFoundError(message);
        default:
          throw new ApiError(code, message, field);
      }
    }

    return json.data;
  }
  ```

  ```python Python theme={null}
  import requests

  def create_campaign(body):
      res = requests.post(
          "https://api.interchange.io/api/v2/buyer/campaigns",
          json=body,
          headers={"Authorization": f"Bearer {SCOPE3_API_KEY}"},
      )
      payload = res.json()

      if not res.ok:
          err = payload["error"]
          if err["code"] == "VALIDATION_ERROR":
              issues = err.get("details", {}).get("issues", [])
              raise ValidationError(err["message"], issues)
          if err["code"] == "RATE_LIMITED":
              raise RateLimitError(int(res.headers.get("Retry-After", "5")))
          raise ApiError(err["code"], err["message"])

      return payload["data"]
  ```
</CodeGroup>

<Warning>
  Don't pattern-match on `message` text — message strings may be reworded for
  clarity. Always branch on `error.code` (and on HTTP status as a fallback).
</Warning>

## Retrying safely

`RATE_LIMITED`, `INTERNAL_ERROR`, and `SERVICE_UNAVAILABLE` are transient — retry GETs with exponential backoff. For creation/mutation requests after a 5xx, prefer to surface the error rather than auto-retry, since duplicate-create protection isn't enforced server-side.

`VALIDATION_ERROR`, `NOT_FOUND`, `FORBIDDEN`, `ACCESS_DENIED`, and `CONFLICT` are terminal — don't retry until the input or state changes.
