# Standard responses

> Every answer carries status, reason and code from one catalog. The tables below are generated from it.

Every JSON answer carries the same fields (the one exception: an unknown path answers a plain `{"error": "Not found"}`):

| Field | What it is |
| --- | --- |
| `status` | The outcome, always one of: `accepted`, `saved`, `pending`, `held`, `rejected`, `invalid`, `failed`. |
| `reason` | A short sentence-case explanation: `Missing HS grad year`, `Not eligible - Program`, `Duplicate - Client`. |
| `code` | A stable upper-case code. Branch on it; it never changes meaning. |
| `retryable` | Whether the same request may succeed later without a change. |
| `field` / `fields` | The field(s) at fault, when there are any. |
| `level` | For an eligibility refusal: where it applied (`offer`, `program`, `bucket`, `campaign`, `affiliate`, `buyer`). |
| `source` | For `DUPLICATE`: `icon` (ICON already has the lead) or `buyer` (the school's system said duplicate). |

## Statuses

| status | Meaning |
| --- | --- |
| `accepted` | The request succeeded: a lead was sold, or a read answered. |
| `saved` | The lead was stored; nothing was sold. |
| `pending` | The buyer has not decided yet. |
| `held` | A test lead, held by ICON before any buyer received it. |
| `rejected` | A valid request that ICON or the buyer declined (eligibility, cap, duplicate, …). |
| `invalid` | The request must change before it can succeed (a missing or bad field, consent, authentication). |
| `failed` | Nothing was decided (a timeout, a buyer or platform error); it may be retried. |

## Reasons

A reason ends in `- Client` when the school's own system decided, and in `- <Level>` (`- Offer`, `- Program`, …) when ICON's rules at that level decided. A reason names a field when one is at fault: `Invalid ZIP code`. Reasons never carry the buyer's own words, internal rule names, limits or ids.

## Codes

The HTTP column lists every status a code can be served with. Decisions about a lead (accepted, refused, held) come with HTTP `200`: read `status` and `code`.

### Success

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `OK` | accepted | OK | no | 200 |
| `ACCEPTED` | accepted | Lead accepted | no | 200 |
| `SAVED` | saved | Lead saved | no | 200 |
| `PENDING` | pending | Pending - Client | no | 200, 202 |
| `TEST_LEAD_HELD` | held | Test lead held | no | 200 |

### Authentication and access

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `AUTH_REQUIRED` | invalid | Missing API key | no | 401 |
| `AUTH_INVALID` | invalid | Invalid API key | no | 401 |
| `FORBIDDEN` | invalid | Not permitted | no | 403 |
| `NOT_FOUND` | invalid | Not found | no | 200, 400, 404 |

### Request

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `METHOD_NOT_ALLOWED` | invalid | Method not allowed | no | 405 |
| `MALFORMED_REQUEST` | invalid | Malformed request | no | 400, 409 |
| `REQUIRED_FIELD_MISSING` | invalid | Missing required field | no | 200, 400, 422 |
| `INVALID_FIELD` | invalid | Invalid field | no | 200, 400, 422 |
| `BOT_CHECK_FAILED` | invalid | Bot check failed | no | 400 |
| `RATE_LIMITED` | failed | Too many requests | yes | 429 |

### Eligibility

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `NOT_ELIGIBLE` | rejected | Not eligible | no | 200 |
| `OFFER_UNAVAILABLE` | rejected | Offer unavailable | no | 200, 400, 404, 410 |

### Caps

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `CAP_REACHED` | rejected | Cap reached | no | 200 |
| `CAP_UNAVAILABLE` | failed | Cap check unavailable | yes | 200, 503 |

### Consent

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `CONSENT_REQUIRED` | invalid | Missing TCPA consent | no | 200, 400 |
| `CONSENT_INVALID` | invalid | Invalid TCPA consent | no | 400, 404, 409 |

### Duplicate

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `DUPLICATE` | rejected | Duplicate - Offer | no | 200, 409 |

### Buyer decision

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `BUYER_REJECTED` | rejected | Rejected - Client | no | 200, 502 |
| `BUYER_INVALID_FIELD` | invalid | Invalid field - Client | no | 200, 502 |
| `BUYER_NOT_ELIGIBLE` | rejected | Not eligible - Client | no | 200, 502 |
| `BUYER_ERROR` | failed | Error - Client | no | 200, 502 |

### Transport

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `TIMEOUT` | failed | Timeout - Client | yes | 200, 504 |

### Platform

| code | status | Default reason | Retryable | HTTP |
| --- | --- | --- | --- | --- |
| `UNAVAILABLE` | failed | Temporarily unavailable | yes | 200, 503 |
| `INTERNAL_ERROR` | failed | Internal error | no | 500 |

## Buyer reasons

When a school's system refuses a lead, ICON translates its answer into one of these standardized reasons (a school that says it is full answers `CAP_REACHED`, `Cap reached`, like any other cap):

| reason | code | field |
| --- | --- | --- |
| Rejected - Client | `BUYER_REJECTED` | — |
| Duplicate - Client | `DUPLICATE` | — |
| Location not served - Client | `BUYER_NOT_ELIGIBLE` | `zip` |
| Age not eligible - Client | `BUYER_NOT_ELIGIBLE` | `date_of_birth` |
| Education not eligible - Client | `BUYER_NOT_ELIGIBLE` | `education_level` |
| HS grad year not eligible - Client | `BUYER_NOT_ELIGIBLE` | `grad_year` |
| Program not offered - Client | `BUYER_NOT_ELIGIBLE` | `program_id` |
| Invalid phone number - Client | `BUYER_INVALID_FIELD` | `phone` |
| Invalid email - Client | `BUYER_INVALID_FIELD` | `email` |
| Invalid ZIP code - Client | `BUYER_INVALID_FIELD` | `zip` |
| Invalid TrustedForm token - Client | `BUYER_INVALID_FIELD` | `trustedform_cert_url` |
| Consent not accepted - Client | `BUYER_REJECTED` | — |
| Test lead - Client | `BUYER_REJECTED` | — |

The `field` column uses ICON's short field names (see the [field dictionary](https://developers.iconroute.io/fields/)). A buyer answer ICON cannot map reads `Rejected - Client`.
