# Errors

The error body every Simple Feedback API route returns, every error code it can carry, and what your integration should do with each one.

Every failed `/v1` request answers with the same body:

```json
{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "This API key is missing the \"insights:write\" scope",
    "details": {
      "requiredScope": "insights:write",
      "grantedScopes": ["surveys:read", "responses:read"]
    }
  }
}
```

- `code` is stable. Branch on it.
- `message` is for people and may change. Don't parse it.
- `details` carries extra facts for some codes and is `{}` otherwise.

## What to do with a status

| Status | Meaning                                            | What to do                              |
| ------ | -------------------------------------------------- | --------------------------------------- |
| `400`  | The request doesn't validate                       | Fix the request; retrying won't help    |
| `401`  | The key is missing, wrong, revoked or expired      | Fix or replace the key                  |
| `403`  | The key works but isn't allowed this               | Fix the scopes, the plan or the account |
| `404`  | The survey or venue doesn't exist for this key     | Check the id                            |
| `409`  | A limit or state stops the change                  | Read the code; most need a person       |
| `422`  | The `Idempotency-Key` was used for another request | Use a new key for a new request         |
| `429`  | Too many requests                                  | Wait `Retry-After` seconds, then retry  |
| `5xx`  | Something failed on our side                       | Retry later with backoff                |

## Codes

### Authentication

| Code              | Status | When                                                                           |
| ----------------- | ------ | ------------------------------------------------------------------------------ |
| `API_KEY_MISSING` | 401    | No `Authorization: Bearer` header                                              |
| `API_KEY_INVALID` | 401    | The token is malformed, unknown, or its secret doesn't match                   |
| `API_KEY_REVOKED` | 401    | The key was revoked, or the person who created it lost the access it acts with |
| `API_KEY_EXPIRED` | 401    | The key is past its expiry date                                                |

`API_KEY_REVOKED` doesn't say whether someone revoked the key or its creator left. The holder of a key isn't always
the person who created it, so nothing about that person is disclosed.

### Permissions and plan

| Code                      | Status | When                                                                                                                    |
| ------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------- |
| `INSUFFICIENT_SCOPE`      | 403    | The key lacks the scope. `details` names the required and granted scopes                                                |
| `API_ACCESS_NOT_IN_TIER`  | 403    | The organization's plan doesn't include API access                                                                      |
| `ORG_SUSPENDED`           | 403    | The organization isn't active                                                                                           |
| `FEATURE_NOT_IN_TIER`     | 403    | The request uses a feature the plan doesn't include, for example custom branding                                        |
| `INSUFFICIENT_VENUE_ROLE` | 403    | A venue key reached for a survey or QR code of another venue                                                            |
| `EMAIL_NOT_VERIFIED`      | 403    | Publishing with `PATCH` while the key creator's email address isn't verified (`details.reason` is `EMAIL_NOT_VERIFIED`) |

### Not found and validation

| Code                        | Status | When                                                                             |
| --------------------------- | ------ | -------------------------------------------------------------------------------- |
| `SURVEY_NOT_FOUND`          | 404    | No such survey, or it belongs to another organization                            |
| `QR_CODE_NOT_FOUND`         | 404    | No such QR code, or it belongs to another organization                           |
| `QR_CODE_NOT_A_SURVEY_CODE` | 400    | `surveyId` sent for a `redirect` or `menu` code, which can't open a survey       |
| `ACCESS_CODE_NOT_FOUND`     | 404    | The code sent to `/access-codes/qr` is not one of this survey's single-use codes |
| `VALIDATION_FAILED`         | 400    | The body or query doesn't validate; `message` names the first problem            |
| `VENUE_OUTSIDE_ORG`         | 403    | The `venueId` you sent belongs to another organization                           |
| `UNSUPPORTED_IMAGE_TYPE`    | 400    | The uploaded image isn't PNG, JPEG or WebP                                       |
| `INVALID_IMAGE`             | 400    | The uploaded bytes aren't a readable image                                       |

### Undo

| Code                   | Status | When                                                                                          |
| ---------------------- | ------ | --------------------------------------------------------------------------------------------- |
| `NOTHING_TO_UNDO`      | 409    | No change made through the API or MCP is recorded for the survey; dashboard edits don't count |
| `SURVEY_CHANGED_SINCE` | 409    | Someone edited the survey after the change you want to undo                                   |

### Plan copies

| Code                    | Status | When                                                         |
| ----------------------- | ------ | ------------------------------------------------------------ |
| `POLL_ALREADY_REPLACED` | 409    | The survey was already replaced by a copy that fits the plan |
| `QR_CODE_NOT_ON_POLL`   | 400    | A code in `qrCodeIds` doesn't point at the survey            |

### Idempotency keys

| Code                          | Status | When                                                                                      |
| ----------------------------- | ------ | ----------------------------------------------------------------------------------------- |
| `IDEMPOTENCY_KEY_REUSED`      | 422    | The key was already used with a different body or on a different route                    |
| `IDEMPOTENCY_KEY_IN_PROGRESS` | 409    | The first request with this key is still running; retry after `details.retryAfterSeconds` |

### Limits

| Code                        | Status | When                                                                                             |
| --------------------------- | ------ | ------------------------------------------------------------------------------------------------ |
| `TIER_LIMIT_REACHED`        | 409    | A plan limit is full: active surveys, pages, languages and so on                                 |
| `AI_QUOTA_EXCEEDED`         | 409    | The plan's AI quota for the period is used up                                                    |
| `ACCESS_CODE_LIMIT_REACHED` | 409    | The survey would have more than 10 000 single-use codes                                          |
| `FILE_TOO_LARGE`            | 413    | An uploaded image is over 2 MB                                                                   |
| `RATE_LIMITED`              | 429    | Too many requests. `details.retryAfterSeconds` and the `Retry-After` header say how long to wait |

Don't retry `409` limits automatically. They clear when someone upgrades the plan, deactivates a survey, or a new
quota period starts.

### Created but not published

Creating a survey with `active: true` creates it first and then publishes it. When publishing fails, for example
because the key creator's email address isn't verified, the survey still exists as a draft and the call answers
`409` with its id. For an unverified email address the code is `EMAIL_NOT_VERIFIED`:

```json
{
  "error": {
    "code": "EMAIL_NOT_VERIFIED",
    "message": "Please verify your email before publishing polls. The survey was created as a draft and was not published.",
    "details": {
      "reason": "EMAIL_NOT_VERIFIED",
      "createdSurveyId": "a1b2c3d4-0001-4001-8001-000000000001"
    }
  }
}
```

> **Warning:** Don't repeat the `POST`, not even with the same `Idempotency-Key`: the call failed, so the key isn't
> kept, and a repeat creates a second draft. Publish `details.createdSurveyId` with `PATCH`
> once the problem is fixed, or delete it.

### Server errors

| Code             | Status | When                                                      |
| ---------------- | ------ | --------------------------------------------------------- |
| `UPSTREAM_ERROR` | 502    | A service we depend on failed, today only the AI provider |
| `INTERNAL_ERROR` | 500    | A bug on our side                                         |

Server errors always carry a generic message and empty `details`. The cause is recorded on our side; if you contact
support, tell us the time and the route.

## The order of checks

Every request is checked in this order before the route runs. The first failure is what you get back:

1. `Authorization` header missing → `401 API_KEY_MISSING`
2. Token malformed, unknown or wrong → `401 API_KEY_INVALID`
3. Key revoked → `401 API_KEY_REVOKED`
4. Key expired → `401 API_KEY_EXPIRED`
5. The creator is no longer active, no longer a member, or no longer has the access the key acts with →
   `401 API_KEY_REVOKED`
6. Organization not active → `403 ORG_SUSPENDED`
7. Plan without API access → `403 API_ACCESS_NOT_IN_TIER`

Then the request body and query are validated (`400 VALIDATION_FAILED`), and only then does the route check the
key's scope (`403 INSUFFICIENT_SCOPE`) and do its work.
