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

# Errors

> The error envelope, every status the API returns, and how to handle conflicts.

## The error envelope

Every failure, from every endpoint, has the same shape.

```json theme={null}
{
  "error": {
    "code": "INVALID_INPUT",
    "message": "Request validation failed.",
    "details": [{ "path": ["callsign"], "message": "Invalid input" }]
  },
  "requestId": "3f1c9d5e-7a42-4f0b-9c1e-8b6d2a4e5f70"
}
```

| Field | Meaning |
| - | - |
| `error.code` | Stable and machine-readable. Branch on this. |
| `error.message` | Written for people. It can change; do not parse it. |
| `error.details` | Only on some errors. See below. |
| `requestId` | Identifies the request in server logs. Also sent as the `X-Request-Id` header on every response, success or failure. |

`details` appears in two cases:

* **Validation failures** carry an array of `{ "path": [...], "message": "..." }`, one per problem. `path` locates the field in the body or query.
* **ER:LC failures** carry `{ "availability": "unavailable" }`.

Internal errors never expose stack traces or database messages.

## Statuses

| Status | Meaning | Codes |
| - | - | - |
| 400 | The request is malformed or breaks a validation rule | `INVALID_INPUT`, `INVALID_JSON`, and rule-specific codes such as `INVALID_TRANSFER` |
| 401 | Not signed in, or the credential is bad | `UNAUTHENTICATED`, `SESSION_EXPIRED`, `AUTHENTICATION_FAILED` |
| 403 | Signed in, but not allowed | `FORBIDDEN`, `USER_REQUIRED`, `INVALID_ORIGIN`, `ACCOUNT_DISABLED`, `STAFF_REQUIRED`, `NOT_UNIT_CREW`, `PROTECTED_PLAYER`, `IDENTITY_MISMATCH` |
| 404 | The record, or the endpoint, does not exist | `NOT_FOUND` |
| 405 | The path exists but not with this method or action | `METHOD_NOT_ALLOWED` |
| 409 | The request is valid but conflicts with the current state | `CAD_CONFLICT`, `CONFLICT`, and state-specific codes listed below |
| 413 | The body is larger than 16 KiB | `BODY_TOO_LARGE` |
| 415 | A body was sent without `Content-Type: application/json` | `UNSUPPORTED_MEDIA_TYPE` |
| 429 | Rate limit exceeded | `RATE_LIMITED` |
| 500 | Something failed inside Argus | `INTERNAL_ERROR` |
| 502 | Discord or Roblox failed during sign-in or linking | `OAUTH_PROVIDER_ERROR`, `INVALID_PROVIDER_RESPONSE` |
| 503 | A dependency is unavailable or not configured | `ERLC_*` codes, `NOT_CONFIGURED`, `LICENCE_NUMBER_UNAVAILABLE` |

Each operation in the reference lists the codes it can return and when.

## Validation: 400

Bodies and most query strings are validated strictly.

* Unknown body properties are rejected. This is how the API refuses a client that tries to name an owner, issuer or actor: those always come from the session.
* Unknown query parameters are rejected on most list endpoints. The CAD list endpoints ignore them instead.
* Text is trimmed before length rules apply.
* A body that is missing or is not JSON returns `INVALID_JSON`.

## Authentication and permission: 401 and 403

| Code | When |
| - | - |
| `UNAUTHENTICATED` | No session cookie, or an invalid, expired or revoked service credential |
| `SESSION_EXPIRED` | The session has ended |
| `FORBIDDEN` | The caller lacks the permission named in the message |
| `USER_REQUIRED` | A service credential called an endpoint that needs a signed-in person |
| `INVALID_ORIGIN` | A browser request other than `GET` came from another origin |
| `ACCOUNT_DISABLED` | The account is suspended or disabled |
| `STAFF_REQUIRED` | The action needs an active staff profile |
| `NOT_UNIT_CREW` | The caller is not active crew on the unit they tried to change |

## Conflicts: 409

A 409 means the request was understood and allowed, but the record is not in a state that permits it. Nothing was changed.

### Stale versions and concurrent changes

CAD incidents and units carry a `version`. Changes send back the version the client last read.

| Code | Meaning |
| - | - |
| `CAD_CONFLICT` | A CAD rule refused the change. The message says which: a stale `version` ("Unit changed. Refresh before saving."), a closed incident, a unit that is not available, a status that does not fit the unit's assignment, and so on. |
| `CONFLICT` | The database refused the change: another request changed the same data at the same moment, a unique value such as a callsign is taken, or a referenced record does not exist. |

For either one: **read the record again, show the person the current state, and let them decide whether to retry.** Argus never retries a conflicting write itself, and a client should not do so blindly. [Optimistic concurrency](/concepts/optimistic-concurrency) covers this in full.

### State conflicts elsewhere

| Area | Codes |
| - | - |
| Sessions and shifts | `SESSION_NOT_ACTIVE`, `SHIFT_NOT_ACTIVE`, `STAFF_INACTIVE` |
| Administration | `SELF_CHANGE`, `RESERVED_ROLE`, `SERVICE_NOT_ACTIVE` |
| Moderation | `IDEMPOTENCY_CONFLICT`, `PLAYER_NOT_ONLINE` |
| Identity | `IDENTITY_ALREADY_LINKED` |
| Civilians | `CIVILIAN_LIMIT`, `CIVILIAN_INACTIVE`, `IDENTITY_LOCKED`, `UNDERAGE`, `LICENCE_EXISTS`, `VEHICLE_LIMIT`, `PLATE_TAKEN` |
| PNC | `INVALID_TRANSITION`, `NO_CHANGE`, `NO_LICENCE`, `RECORD_MISMATCH`, `RECORD_VOID`, `ENDORSEMENT_NOT_ACTIVE`, `WARRANT_NOT_ACTIVE`, `MARKER_EXISTS`, `MARKER_NOT_ACTIVE` |

## Rate limits: 429

`RATE_LIMITED` comes with a `Retry-After` header in whole seconds. Wait that long before the next request. The budgets are listed under [Authentication](/authentication#rate-limits).

## ER:LC unavailable: 503

Live server data and remote moderation depend on the ER:LC API.

| Code | When |
| - | - |
| `ERLC_THROTTLED` | Another Argus request to ER:LC is in flight, or a cooldown is running |
| `ERLC_RATE_LIMITED` | ER:LC's own rate limit was hit |
| `ERLC_AUTH_FAILED` | ER:LC rejected the server key. Argus pauses its requests for an hour. |
| `ERLC_UNAVAILABLE`, `ERLC_NETWORK_ERROR`, `ERLC_INVALID_RESPONSE` | ER:LC failed, did not answer, or answered with something unusable |
| `NOT_CONFIGURED` | The deployment has no ER:LC server key, or no OAuth client for the provider being used |

Argus caches a good snapshot for ten seconds and never serves expired data after a failure. Cooldown responses include `Retry-After`.

## Moderation is different: read `delivery`

A kick or ban returns **success** as soon as the punishment record exists. Whether the game carried it out is in the record's `delivery` field: `SUCCEEDED`, `FAILED` with an `errorCode`, or `UNKNOWN` when the outcome could not be confirmed. An `UNKNOWN` kick or ban may have happened. Argus never resends a command.

These three endpoints also require an `Idempotency-Key` header. Repeating a request with the same key and body returns the original record; the same key with a different body returns `409 IDEMPOTENCY_CONFLICT`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.