# Errors

All errors use the envelope from [Conventions](./conventions.md). `error.code` is stable; `error.message` may change.

## Auth

| Code                       | HTTP | When                                                   |
| :------------------------- | :--: | :----------------------------------------------------- |
| `API_KEY_REQUIRED`         | 401  | No `Authorization` header.                             |
| `INVALID_API_KEY`          | 401  | Format wrong or hash mismatch.                         |
| `API_KEY_REVOKED`          | 401  | Key was revoked.                                       |
| `API_KEY_EXPIRED`          | 401  | Past `expiresAt`.                                      |
| `API_KEY_SCOPE_DENIED`     | 403  | Key lacks the required scope.                          |
| `ACCOUNT_RATE_LIMIT_EXCEEDED` | 429  | Per-account rate limit hit.                         |
| `AUTH_RATE_LIMIT_EXCEEDED` | 429  | Per-IP rate limit hit without a valid key.             |
| `IGP24_SUBMISSION_RATE_LIMIT_EXCEEDED` | 429  | Per-team IGP24 submission quota hit. |

## Request shape

| Code                          | HTTP | When                                                                        |
| :---------------------------- | :--: | :-------------------------------------------------------------------------- |
| `MALFORMED_BODY`              | 400  | JSON failed to parse, or contained unknown top-level fields.                |
| `NOT_FOUND`                   | 404  | The requested resource does not exist or is not visible to you.             |
| `SUBMISSION_PAYLOAD_INVALID`  | 422  | `payload` failed `submissionSpec.schema`; `details.fieldErrors[]` says what.|
| `SUBMISSION_META_INVALID`     | 422  | `meta` failed `submissionSpec.metaSchema`; `details.fieldErrors[]` says what.|
| `SUBMISSION_SLOT_INVALID`     | 422  | `slot` is not an integer from `1` through `submissionSpec.limits.maxEntries`.|
| `NO_DOWNLOAD_FOR_KIND`        | 404  | The submission kind has no downloadable text body.                          |
| `RUN_PARAMS_INVALID`          | 422  | Run references an unknown model, problem, or artifact ID.                   |
| `INVALID_MODEL_REFERENCE`     | 422  | Model-reference run fields failed repository, commit, or problem-set validation. |
| `RESOURCE_FIELD_INVALID`      | 422  | A resource field, filter, or enum value failed endpoint-specific validation.|
| `INVALID_CASE`                | 422  | `caseId` is not a valid case for the competition's playground check.        |
| `LEAN_SOURCE_INVALID`         | 422  | `solverCode` or custom-problem text failed the Lean 4 parser.               |
| `POLYNOMIAL_INPUT_INVALID`     | 422  | IGP24 polynomial input failed validation (e.g. empty list, or a coefficient line failed format checks). |
| `COMMENT_BODY_INVALID`        | 422  | Comment body empty or over the length limit.                                |

`fieldErrors[]` entries are JSON Pointers into the object named by the code:

```json
{
  "code": "SUBMISSION_PAYLOAD_INVALID",
  "message": "payload failed validation",
  "details": {
    "fieldErrors": [
      { "path": "/modelName", "code": "PATTERN_MISMATCH", "message": "Must match Hugging Face repo ID pattern" },
      { "path": "/commitHash", "code": "REQUIRED", "message": "Required" }
    ]
  }
}
```

## Business rules

Shared across SAIR — same code, same meaning, same HTTP everywhere it appears.

| Code                                | HTTP | When                                                                       |
| :---------------------------------- | :--: | :------------------------------------------------------------------------- |
| `ENROLL_REQUIRED`                   | 403  | You haven't enrolled. Enroll at `https://competition.sair.foundation/competitions/{competitionId}`. |
| `TEAM_OWNER_REQUIRED`               | 403  | Caller lacks the team role required by `submissionSpec.permission`.        |
| `EMAIL_NOT_VERIFIED`                | 403  | Your account email isn't verified. Manage in [SAIR ID](https://id.sair.foundation). |
| `SUBMISSION_WINDOW_NOT_OPEN`        | 403  | Submission window hasn't opened yet.                                       |
| `SUBMISSION_WINDOW_CLOSED`          | 403  | Submission window has closed.                                              |
| `FILE_TOO_LARGE`                    | 413  | Payload exceeds `submissionSpec.limits.maxBytes`.                          |
| `COMPANION_COMPETITION_REQUIRED`    | 403  | This competition is gated to participants enrolled in a companion competition for the current season. |
| `CHEATSHEET_LOCKED`                 | 403  | Cheatsheet is published on the Contributor Network. Withdraw it first.   |
| `SOLVER_TEMPLATE_LOCKED`            | 403  | Solver template is published on the Contributor Network. Withdraw it first.       |
| `RUN_BUDGET_EXCEEDED`               | 403  | Run would exceed your remaining credit balance.                            |
| `RUN_NOT_CANCELLABLE`               | 409  | Run is already `done`, `failed`, or `cancelled`.                           |
| `ACTIVE_RUN_EXISTS`                 | 409  | A model-reference run is already queued or running for this account.       |
| `RESOURCE_NOT_OWNED`                | 403  | The source resource you tried to publish isn't owned by you.               |
| `ITEM_NOT_AUTHOR`                   | 403  | You can only withdraw / delete items you authored.                         |
| `COMMENT_NOT_AUTHOR`                | 403  | You can only delete comments you wrote or that target your own item.       |
| `GRAPH_TOO_LARGE`                   | 413  | Lineage graph response would exceed 2 000 nodes — narrow with filters.     |
| `IGP24_VERIFY_UNAVAILABLE`          | 503  | IGP24 verifier service is not configured or unavailable.                   |
| `IGP24_VERIFY_TIMEOUT`              | 504  | IGP24 verifier service timed out.                                          |
| `IGP24_VERIFY_FAILED`               | 502  | IGP24 verifier service returned an invalid or failed response.             |

## `submitBlockedReason` → error code

[`GET /api/public/v1/competitions/{competitionId}/me`](./endpoints/competitions.md#my-participation) returns a `submitBlockedReason` enum so clients can decide whether to even attempt a submission. Attempt it anyway and you'll get the equivalent error:

| `submitBlockedReason` | Equivalent `error.code` on `POST /competitions/{competitionId}/submissions` |
| :-------------------- | :--------------------------------------------------------------------------- |
| `EMAIL_NOT_VERIFIED`  | `EMAIL_NOT_VERIFIED`                                                         |
| `NOT_ENROLLED`        | `ENROLL_REQUIRED`                                                            |
| `NOT_TEAM_OWNER`      | `TEAM_OWNER_REQUIRED`                                                        |
| `BEFORE_WINDOW`       | `SUBMISSION_WINDOW_NOT_OPEN`                                                 |
| `AFTER_WINDOW`        | `SUBMISSION_WINDOW_CLOSED`                                                   |
