---
title: "Errors and problem details"
description: "The MainSDK error shape - RFC 9457 problem details, the MainError class, stable MAIN- codes, redaction, mainsdk explain and MAINSDK_DEBUG."
section: "reference"
status: "Planned for 0.2"
spec: "errors"
canonical: "https://mainsdk.dev/docs/reference/errors"
updated: "2026-10-28"
---

# Errors and problem details

> The MainSDK error shape - RFC 9457 problem details, the MainError class, stable MAIN- codes, redaction, mainsdk explain and MAINSDK_DEBUG.

Status: Planned for 0.2 · Canonical: https://mainsdk.dev/docs/reference/errors · Updated: 2026-10-28

Every error MainSDK raises tells you, or your agent, what failed, where in your own code, which `main.yaml` rule was involved and how to fix it. Errors follow open standards so any tool can read them:

| Standard | Used for |
| --- | --- |
| RFC 9457 Problem Details | Every API error body, `Content-Type: application/problem+json` |
| OpenTelemetry logs data model and semantic conventions | Every log record, exception attributes and code location |
| W3C Trace Context | `trace_id` links an error to its request, job and agent change |
| RFC 3339 | Timestamps, in UTC |

## The problem body

```json title="403 application/problem+json"
{
  "type": "https://mainsdk.dev/errors/MAIN-DATA-0007",
  "title": "Record belongs to another user",
  "status": 403,
  "detail": "orders.get was called for an order owned by a different user.",
  "instance": "/api/orders/8812",
  "code": "MAIN-DATA-0007",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "rule": "data.owner_scope",
  "fix": "Scope the query with db.orders.for(user).",
  "retryable": false
}
```

| Field | Standard or extension | Meaning |
| --- | --- | --- |
| `type` | RFC 9457 | The docs page for the code; it always resolves |
| `title` | RFC 9457 | Short summary, the same for every occurrence of the code |
| `status` | RFC 9457 | HTTP status |
| `detail` | RFC 9457 | What happened this time, redacted |
| `instance` | RFC 9457 | The request path |
| `code` | MainSDK | Stable code, `MAIN-<AREA>-<NNNN>` |
| `trace_id` | MainSDK | The W3C trace ID from the request's `traceparent` |
| `rule` | MainSDK | The `main.yaml` or policy rule involved, when there is one |
| `fix` | MainSDK | What to change, in one sentence |
| `retryable` | MainSDK | Whether retrying can help |
| `function`, `file`, `line` | MainSDK | Where in your code; in logs always, in response bodies only outside production |

In production, response bodies sent to end users never carry `file`, `line`, `function` or a stack trace. Those stay in your logs.

## Throw a MainError

`MainError` builds the body for you. Codes are a generated union type, so a typo doesn't compile.

```ts title="app/lib/orders.ts"
import { MainError } from "mainsdk";

throw new MainError("MAIN-DATA-0007", {
  rule: "data.owner_scope",
  fix: "Scope the query with db.orders.for(user).",
});
```

Inside a route wrapped by an adapter, such as `withMainSDK` from `mainsdk/next`, the thrown error becomes the problem body above, with status 403 and the request's `trace_id`. An unknown error, such as a `TypeError`, becomes `MAIN-CORE-0500` with no internal detail in production. Outside an adapter, call `err.toProblem(instance)` yourself.

```ts
// mainsdk/log
class MainError extends Error {
  constructor(code: ErrorCode, opts?: { detail?: string; fix?: string; cause?: unknown;
    rule?: string; retryable?: boolean; status?: number });
  readonly code: ErrorCode;          // "MAIN-<AREA>-<NNNN>"
  toProblem(instance?: string): Problem;
}

interface Problem {                  // application/problem+json, RFC 9457
  type: string; title: string; status: number; detail: string; instance?: string;
  code: ErrorCode; trace_id?: string; function?: string; file?: string; line?: number;
  rule?: string; fix?: string; retryable: boolean;
}
```

## The same error in your logs

Each error is also a log record with OpenTelemetry exception attributes and code location, linked to its request by the trace ID, so your agent can find the cause without guessing:

```json title="mainsdk logs --json"
{
  "timestamp": "2026-11-03T10:14:02Z",
  "severity": "ERROR",
  "body": "Record belongs to another user",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "attributes": {
    "exception.type": "MainError",
    "exception.message": "orders.get was called for an order owned by a different user.",
    "code.function.name": "getOrder",
    "code.file.path": "app/api/orders/[id]/route.ts",
    "code.line.number": 18
  }
}
```

The `exception.*` and `code.*` names are OpenTelemetry's. The attribute names for MainSDK's own fields (`code`, `rule`) come with the `telemetry@1` contract.

Stack frames point at your source, not at SDK internals, through the source maps shipped with every package.

## What is redacted

Before an error is written, returned or exported, MainSDK replaces:

- values of fields labelled `personal`, `sensitive`, `child` or `phi` with `[redacted:personal]` and so on;
- anything resolved from a `secret:` reference, and known key formats such as `msk_live_…`, with `[redacted:secret]`.

## Codes

| Part | Rule |
| --- | --- |
| `MAIN` | Fixed prefix |
| `<AREA>` | One of `AUTH`, `ACCESS`, `DATA`, `CONFIG`, `SECRET`, `POLICY`, `NET`, `LOG`, `CORE`, `CONTRACT`, `EMAIL`, `PAY`, `FILES`, `WEBHOOK`, `JOBS`, `AI`, `SCHEMA`, `API`, `IMPORT`, `CONN`, `SEED`, `PREVIEW`, `ENGAGE`, `ANALYTICS`, `METER`, `LIMIT`, `COMPUTE`, `MAIL`, `CAL`, `SHARE`, `LINK`, `DOC`, `SANDBOX`, `I18N`, `VOICE`, `EVAL`, and later interfaces |
| `<NNNN>` | Four digits, assigned once, never reused or changed in meaning |

Every code is declared once in the contract source with its title, HTTP status, default fix, retryable flag and docs text. Removing a code, or changing what it means, is a breaking change that `mainsdk contract diff` fails. Every code defined so far, including areas other specs add (such as `KEYS`, `PRIV`, `NOTIFY`, `MCP`, `REALTIME` and `HOOK`), is listed in [check codes](https://mainsdk.dev/docs/reference/check-codes).

## Runtime errors from seal and apps

These runtime codes come from the build seal (spec `seal`) and the app registry (spec `app-registry`). Every other code is in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |
| `MAIN-SEAL-0001` | The files on disk don't match the sealed build hash. Logged in warn mode; the app refuses to start in refuse mode |
| `MAIN-SEAL-0002` | No seal, or an unreadable one, outside dev while `seal.mode` isn't `off` |
| `MAIN-APPS-0001` | The registry folder is unsafe or not writable; nothing is written and the app keeps serving |
| `MAIN-APPS-0006` | A peer call between apps that don't both list each other under `apps.peers` |
| `MAIN-APPS-0007` | A peer call to an action the callee doesn't list under `apps.expose` |
| `MAIN-APPS-0008` | The link handshake failed: the peer's proof doesn't verify, or its seal is invalid |
| `MAIN-APPS-0009` | `apps.link: on` without `apps.identity` and `MAINSDK_APP_KEY`; the link stays off |

## Runtime errors from memory

These runtime codes come from agent memory (spec `memory`, planned for 0.4). The check findings `MAIN-MEM-0020` to `MAIN-MEM-0025` are in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |
| `MAIN-MEM-0001` | No subject: `about` is missing and the call has no `on_behalf_of` or session user (400) |
| `MAIN-MEM-0002` | No memory with that id in the current tenant, app and scope (404, same body either way) |
| `MAIN-MEM-0003` | The injection check flagged the text of a write; nothing was stored (422) |
| `MAIN-MEM-0004` | A recalled memory was flagged by the injection check and left out of the context (logged) |
| `MAIN-MEM-0005` | The agent has no memory grant for this verb, or `forget: deny` (403) |
| `MAIN-MEM-0006` | The agent is over its memory `recalls` or `writes` per day (429) |
| `MAIN-MEM-0007` | Forget refused: a legal hold applies to the person (409) |
| `MAIN-MEM-0008` | A children's rule refused it: children are off, no guardian consent, or a provider model would extract (403) |
| `MAIN-MEM-0009` | Invalid input: unknown kind, bad duration, `pan` label, `k` over 100, `hops` over 2 or a bad predicate (400) |
| `MAIN-MEM-0010` | Revise of a version that is no longer current (409) |
| `MAIN-MEM-0011` | Import refused: a malformed line, or personal or non-procedure memory crossing a tenant or app (422) |
| `MAIN-MEM-0012` | The driver doesn't declare the capability the call needs (501) |
| `MAIN-MEM-0013` | The memory store is unreachable (503, retryable); `context` degrades to an empty block |
| `MAIN-MEM-0014` | An Anthropic memory tool path is outside `/memories/` or tries to traverse (400) |
| `MAIN-MEM-0015` | A driver hasn't confirmed the erase yet; no receipt is issued and the erase keeps retrying (503) |
| `MAIN-MEM-0016` | A write from a tool, MCP, the web or an import was refused by `from-tools: refuse` (403) |

## Runtime errors from the API and the typed client

These runtime codes come from the specs `app-api`. Each one also has a row in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |

## Runtime errors from imports, connections and MCP

These runtime codes come from the specs `imports`, `connections`. Each one also has a row in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |

## Runtime errors from seed, preview, analytics and engagement

These runtime codes come from the specs `seed-preview`, `analytics`, `engagement`. Each one also has a row in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |

## Runtime errors from workflows, realtime and health

These runtime codes come from the specs `workflows`, `realtime`, `health`. Each one also has a row in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |

## Runtime errors from search, feeds, moderation and media

These runtime codes come from the specs `search`, `feeds`, `moderate` and `media`, all planned for 0.4. Each one also has a row in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |
| `MAIN-SEARCH-0001` | The index isn't declared under `search.indexes` (400) |
| `MAIN-SEARCH-0002` | The access filter can't be expressed in the engine's query; refused, never post-filtered (403) |
| `MAIN-SEARCH-0003` | `search.query` without `for`, outside `tenant.system` (400) |
| `MAIN-SEARCH-0004` | The engine is unreachable or the query passed `timeout` (503, retryable) |
| `MAIN-SEARCH-0005` | A filter, sort or facet names an undeclared or reserved field (400) |
| `MAIN-SEARCH-0006` | No index endpoint for the tenant's region; the change stays queued |
| `MAIN-SEARCH-0007` | A cursor would go past `max-depth` (400) |
| `MAIN-SEARCH-0008` | `q` is too long, or a filter value has the wrong type (400) |
| `MAIN-SEARCH-0009` | An index change is in the dead-letter queue after its last retry |
| `MAIN-FEED-0001` | Refused: one side blocks the other (403) |
| `MAIN-FEED-0002` | A follow or item across tenants (403) |
| `MAIN-FEED-0003` | The item doesn't exist, is removed, or isn't visible to the viewer (404) |
| `MAIN-FEED-0004` | Edit or remove by someone who isn't the author or allowed `feeds.remove` (403) |
| `MAIN-FEED-0005` | Following more than `max-following` (422) |
| `MAIN-FEED-0006` | A fan-out batch is in the dead-letter queue; timelines merge at read time meanwhile |
| `MAIN-FEED-0007` | The ranker failed or was too slow; the chronological timeline was served |
| `MAIN-FEED-0008` | A pushed value loosens a feed limit; refused, last good kept |
| `MAIN-MOD-0001` | Report refused by the rate limit or as a repeat (429) |
| `MAIN-MOD-0002` | Unknown reason, or the target isn't in the current tenant (400) |
| `MAIN-MOD-0003` | The caller lacks `moderate.review`, or the case is someone else's (403) |
| `MAIN-MOD-0004` | Appeal refused: outside the window, a repeat, or not the caller's (409) |
| `MAIN-MOD-0005` | An appeal resolved by the reviewer who decided (403) |
| `MAIN-MOD-0006` | A pre-screen driver failed; the content stays held |
| `MAIN-MOD-0007` | Known-abuse hash match; the content is blocked and never published |
| `MAIN-MOD-0008` | A decision without a ground or facts (400) |
| `MAIN-MOD-0009` | Held, blocked or removed content was read (403) |
| `MAIN-MEDIA-0001` | The source file isn't ready, isn't in this tenant, or isn't readable by the caller (404) |
| `MAIN-MEDIA-0002` | The source is over a duration, size or pixel limit (422) |
| `MAIN-MEDIA-0003` | The source's type isn't supported or doesn't match its declared type (415) |
| `MAIN-MEDIA-0004` | The driver doesn't support this kind or format (501) |
| `MAIN-MEDIA-0005` | A transform step failed after its last retry; the job is `failed` |
| `MAIN-MEDIA-0006` | No job with that id in the current tenant (404) |
| `MAIN-MEDIA-0007` | `media.keep` lists location or an unknown metadata name; start fails |
| `MAIN-MEDIA-0010` | The `ffmpeg` driver in production; logged once at start |

## Runtime errors from protect

These runtime codes come from protected files (spec `protect-taps`, planned for 0.4). A denial carries the TAPS error type in a `taps` member and never names the failed condition. Each one also has a row in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |
| `MAIN-PROTECT-0001` | Licence denied by the policy (403); the TAPS error type is in `taps`, the failed condition only in the decision log |
| `MAIN-PROTECT-0002` | Time-locked: before `releaseAt`, or the release round isn't available yet (403, `retryAfter`) |
| `MAIN-PROTECT-0003` | The asset, the person's access, the link or the licence was revoked, or the asset was erased (403; 410 when erased) |
| `MAIN-PROTECT-0004` | No such asset, version, link, delegation or approval in this tenant (404, same body either way) |
| `MAIN-PROTECT-0005` | A person must approve first; the problem carries `approvalId` and `retryAfter` (403) |
| `MAIN-PROTECT-0006` | The approver declined, or the approval timed out (403) |
| `MAIN-PROTECT-0007` | Delegation invalid: wider than the person, expired, revoked, used up or deeper than 3 (403) |
| `MAIN-PROTECT-0008` | A query or play budget, or a delegation's `maxUses`, is used up (429) |
| `MAIN-PROTECT-0009` | A session, print, device or recipient-verification rate limit was passed (429, `retryAfter`) |
| `MAIN-PROTECT-0010` | Suite not supported: `TAPS-CS3` is refused until it is registered; no post-quantum protection is claimed (400) |
| `MAIN-PROTECT-0011` | Threshold release not reached: fewer than `t` parties have released (403, retryable) |
| `MAIN-PROTECT-0012` | The asset or its label is locked down, suspended or quarantined (423) |
| `MAIN-PROTECT-0013` | Step-up required: the policy needs a fresher or stronger sign-in, listed in `requirements` (401) |
| `MAIN-PROTECT-0014` | The TAPS Node, a threshold party or the sealing service is unreachable (503, retryable); nothing opens without a licence |
| `MAIN-PROTECT-0015` | Invalid input: unknown policy key, label `pan`, a zone id that isn't opaque, `t` outside 2 to n, `releaseAt` in the past, an unknown party or an undeclared agent (400) |
| `MAIN-PROTECT-0016` | The file's version is superseded; `location` names the current version (409) |
| `MAIN-PROTECT-0017` | The ledger is stale or the registration is pending (503, `retryAfter`) |
| `MAIN-PROTECT-0018` | A query answer was refused by the output controls: the whole document, a verbatim span over the limit, or a denied field (422) |
| `MAIN-PROTECT-0019` | The driver's profile lacks the capability the call needs, such as `query` or `web3` (501) |

## Runtime errors from licence

These runtime codes come from app licensing (spec `licence`, built; codes from 0020 come from MainSDK Cloud, spec `licence-cloud`, planned for 0.6). A refusal never includes the licence's claims, its id or the machine id. Each one also has a row in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |
| `MAIN-LICENCE-0001` | The feature needs a licence and the licence in scope doesn't include it, or isn't valid or in grace (403, with `licence: { feature, status, reason }`) |
| `MAIN-LICENCE-0002` | A licensed limit is reached: the licence's ceiling is the one that binds (403, with `licence: { name, limit }`) |
| `MAIN-LICENCE-0003` | A licensed limit was passed with `over: report`; the call was allowed. Logged once per limit and scope per UTC day |
| `MAIN-LICENCE-0004` | A licence, CA certificate or revocation list doesn't verify; `reason` is `signature`, `chain`, `format`, `subject`, `binding`, `rollback` or `test-root` (400 from `licence.update()`) |
| `MAIN-LICENCE-0005` | The licence is in grace: expiring, revoked or suspended. Warned once a day with `graceEndsAt` |
| `MAIN-LICENCE-0006` | The licence is expired, revoked or suspended past its grace; licensed features are off |
| `MAIN-LICENCE-0007` | No licence, or not yet valid; licensed features are off |
| `MAIN-LICENCE-0008` | Refresh failed, or the newest revocation list is past its `nextUpdate`; the current state is kept. Logged once per outage |
| `MAIN-LICENCE-0009` | An older licence (`seq`) or revocation list (`number`), or a different list with the same number, was refused; the newer one stays |
| `MAIN-LICENCE-0010` | The clock is behind the high-water time by more than `licence.skew`; the high-water time is used for licence decisions |
| `MAIN-LICENCE-0013` | A feature name that isn't under `licence.features`: `has` returns false and logs once; `require` throws (500) |
| `MAIN-LICENCE-0015` | The machine id can't be read: no `/etc/machine-id` for `machine: os`, or `MAINSDK_MACHINE_ID` unset for `machine: env` (500) |
| `MAIN-LICENCE-0016` | `licence.update` refused: no `licence.manage` permission, an app licence outside `tenant.system()` in a tenanted app, or a tenant licence for another tenant (403) |
| `MAIN-LICENCE-0020` | Cloud: the caller lacks the role, or a write has no passkey session from the last 5 minutes (403) |
| `MAIN-LICENCE-0021` | Cloud: the licence claims are invalid; `errors` names each field (422) |
| `MAIN-LICENCE-0022` | Cloud: no such root, CA or licence in this account (404) |
| `MAIN-LICENCE-0023` | Cloud: the change conflicts with the state, such as renewing a revoked licence or issuing from a retired CA (409) |
| `MAIN-LICENCE-0024` | Cloud: a second approver is needed; the problem carries `approvalId` |
| `MAIN-LICENCE-0025` | Cloud: more than 60 refresh calls for one licence in an hour (429, `Retry-After`) |
| `MAIN-LICENCE-0026` | Cloud: the key service can't sign; nothing was issued or changed (503, retryable) |

## Runtime errors from share and links

These runtime codes come from `share` (spec `files-share`, ADR-0056) and `links` (spec `links`, planned for 0.4), and from the sign-in link method of `auth`. The check findings `MAIN-SHARE-0001`, `MAIN-SHARE-0007` and `MAIN-LINK-0003` to `MAIN-LINK-0005` are in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |
| `MAIN-SHARE-0008` | `share.driver: local` in production; the app refuses to start |
| `MAIN-SHARE-0009` | A local share link was opened after it expired or was revoked (410) |
| `MAIN-SHARE-0010` | `share.update` was asked to keep a share forever (400) |
| `MAIN-SHARE-0011` | `share.placements.create` failed its row checks and created nothing (422, rows listed) |
| `MAIN-SHARE-0012` | A getqr.id webhook event type outside the pinned contract; acknowledged and logged once |
| `MAIN-SHARE-0013` | Personal data in a share's `title` or `metadata`, too many or reserved metadata keys, or someone else's contact card (400) |
| `MAIN-LINK-0001` | Invite link used up, expired, revoked, or for another email (410) |
| `MAIN-LINK-0002` | A screen or params not declared under `links.routes` (400) |
| `MAIN-LINK-0005` | `links.signIn` without `link` in `auth.methods` (400) |
| `MAIN-AUTH-0046` | Sign-in link used, expired, unknown, or opened in another browser (401) |

## Runtime errors from the modern AI specs

These runtime codes come from multimodal input, images, video, the agent loop, routing and local models (specs `ai-multimodal`, `ai-image`, `ai-video`, `ai-run`, `ai-routing`, `ai-local`), documents (`documents`), sandboxes (`sandbox`), languages (`i18n`) and voice agents (`ai-voice`). Their check findings, and the `EVAL` codes the CLI raises, are in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |
| `MAIN-AI-0040` | A prompt part is invalid: unknown type, no file, a `mime` that doesn't match the bytes, `pages` out of order, or `video` given as bytes (400); nothing is sent |
| `MAIN-AI-0041` | A prompt part is over a limit: size, pixels, pages, duration, frames or `max-parts` (413); nothing is sent |
| `MAIN-AI-0042` | The model or driver doesn't accept this kind of part (501) |
| `MAIN-AI-0043` | A labelled image, audio or scan would go to a driver whose data use or region doesn't meet the rules, and media can't be redacted (403) |
| `MAIN-AI-0045` | An image option is invalid: size, count, mask size or an empty prompt (400); nothing is sent |
| `MAIN-AI-0046` | The image driver doesn't offer this operation (501) |
| `MAIN-AI-0047` | The provider refused the image prompt under its content policy (422) |
| `MAIN-AI-0048` | A content credential couldn't be signed and `ai.credentials.required` is true (500) |
| `MAIN-AI-0050` | A video option is invalid: seconds, size, prompt or references (400); nothing is sent |
| `MAIN-AI-0051` | No video job with that id in the current tenant (404) |
| `MAIN-AI-0052` | The provider failed or refused the video job; the job is `failed` |
| `MAIN-AI-0053` | A video job lost its provider heartbeat and was cancelled at the provider |
| `MAIN-AI-0060` | An `ai.run` option is invalid: goal and messages, `maxSteps`, budget, timeout or agent (400); nothing is sent |
| `MAIN-AI-0061` | The run stopped at `maxSteps` (`stop: "max-steps"`) |
| `MAIN-AI-0062` | The run stopped at its budget (`stop: "budget"`) |
| `MAIN-AI-0063` | The run stopped at its timeout (`stop: "timeout"`) |
| `MAIN-AI-0064` | The model asked for a tool that isn't in the run's `tools`; the step was refused |
| `MAIN-AI-0065` | A handoff or delegation names an agent not declared for it (400) |
| `MAIN-AI-0066` | A delegation asked for more than the parent's remaining budget or the declared share (402) |
| `MAIN-AI-0067` | No run with that id in the current tenant, or it isn't waiting (404 or 409) |
| `MAIN-AI-0071` | Every model in the route failed with a retryable error (502) |
| `MAIN-AI-0072` | A fallback model was skipped because its endpoint is outside the tenant's region for labelled data (logged) |
| `MAIN-AI-0073` | A response cache entry is invalid: tenant scope with labelled data, a TTL over 7 days, or `local` kv in production |
| `MAIN-AI-0074` | A batch option is invalid: no items, too many, duplicate ids, or no batch API (400) |
| `MAIN-AI-0075` | No batch with that id in the current tenant (404) |
| `MAIN-AI-0078` | The local model server is unreachable (503, retryable) |
| `MAIN-AI-0080` | The model isn't on the local server (404) |
| `MAIN-DOC-0001` | The file isn't a supported document type, or its bytes don't match its declared type (415) |
| `MAIN-DOC-0002` | Over a limit: size, pages, ZIP expansion, or more pages than `inline-pages` for `parse` (413) |
| `MAIN-DOC-0003` | Pages have no text layer and OCR is off or no OCR driver is set (422) |
| `MAIN-DOC-0004` | The documents driver doesn't offer this (501) |
| `MAIN-DOC-0005` | The PDF is encrypted or password protected (422) |
| `MAIN-DOC-0006` | Active content was found and not run (logged) |
| `MAIN-DOC-0007` | A render layout or template is invalid (400) |
| `MAIN-DOC-0008` | A labelled document would go to a driver outside the tenant's region or not meeting the data rules (403) |
| `MAIN-DOC-0009` | No document job with that id in the current tenant (404) |
| `MAIN-SANDBOX-0001` | A sandbox option is invalid (400); nothing ran |
| `MAIN-SANDBOX-0002` | The code reached its time limit and was killed |
| `MAIN-SANDBOX-0003` | The code passed its memory limit and was killed |
| `MAIN-SANDBOX-0004` | Output or files passed their limit; output is cut |
| `MAIN-SANDBOX-0005` | A host isn't in `sandbox.network` and `network.allow`, or the code tried a host it wasn't given (403) |
| `MAIN-SANDBOX-0006` | The sandbox driver is unavailable (503, retryable) |
| `MAIN-SANDBOX-0007` | The run would pass, or reached, its cost ceiling (402) |
| `MAIN-SANDBOX-0008` | No sandbox session with that id in the current tenant, or it was closed (404) |
| `MAIN-SANDBOX-0009` | A secret reference isn't under `sandbox.secrets` (403) |
| `MAIN-SANDBOX-0011` | The `local` sandbox driver in production |
| `MAIN-I18N-0004` | A message was missing and a fallback was used (logged once per key and locale) |
| `MAIN-I18N-0005` | `t()` was called without a value its message needs (logged) |
| `MAIN-I18N-0007` | An agent or service tried to approve a translation draft (403) |
| `MAIN-I18N-0009` | No message for the key in any locale; production never shows the key (logged) |
| `MAIN-VOICE-0001` | A voice session option is invalid (400) |
| `MAIN-VOICE-0002` | The voice driver doesn't offer this (501) |
| `MAIN-VOICE-0003` | Recording refused: recording off, or no consent from the caller (403) |
| `MAIN-VOICE-0004` | The session reached its duration or cost limit and ended after a closing line |
| `MAIN-VOICE-0005` | A telephony or room webhook failed signature verification (401) |

## Runtime errors from db and kv

These runtime codes come from `db` (specs `db`, `db-writes` and `db-queries`) and `kv` (spec `kv`). The check findings `MAIN-DATA-0013`, `MAIN-DATA-0020`, `MAIN-DATA-0021`, `MAIN-DATA-0030`, `MAIN-DATA-0040` to `MAIN-DATA-0045`, `MAIN-DATA-0051`, `MAIN-DATA-0055`, `MAIN-DATA-0056`, `MAIN-DATA-0061`, `MAIN-DATA-0062` and `MAIN-DATA-0065` are in [check codes](https://mainsdk.dev/docs/reference/check-codes).

| Code | Meaning |
| --- | --- |
| `MAIN-DATA-0008` | The record isn't there, with tenancy off (404) |
| `MAIN-DATA-0009` | A create repeats an id or a declared unique value (409) |
| `MAIN-DATA-0031` | The local SQLite driver can't start: this Node has no `node:sqlite` (500) |
| `MAIN-DATA-0032` | `db.driver` is not built, or a Postgres driver has no client (400) |
| `MAIN-DATA-0050` | A query ran past its statement timeout and was cancelled (503, not retried) |
| `MAIN-DATA-0052` | No connection was free within `db.pool.acquire` (503 with `Retry-After: 1`) |
| `MAIN-DATA-0053` | A query took longer than `db.slow` (warn log, once a minute per query shape) |
| `MAIN-DATA-0054` | One query shape ran more than `db.repeat` times in one request or job (warn log) |
| `MAIN-DATA-0060` | `ifVersion` doesn't match the record's version (409; 412 from `If-Match`) |
| `MAIN-DATA-0063` | `restore` of a record not in trash, or past its trash period (404) |
| `MAIN-DATA-0066` | A write to a versioned record came with no `If-Match` (428) |
| `MAIN-KV-0001` | `incr` was given a step that is not a whole number (400), or the key holds a value that is not a whole number (409) |
| `MAIN-KV-0002` | `ttl` is not a positive duration (400) |
| `MAIN-KV-0003` | A key is empty or longer than 512 characters (400) |

## Codes from other specs

These runtime codes are defined in a spec's Codes table and listed here automatically. The spec holds the full cause, and `npx mainsdk explain <code>` gives the fix.

<!-- codes:begin (generated by tools/build_codes.py; edit the spec's Codes table, never these rows) -->
| Code | Raised by | Meaning | Spec |
| --- | --- | --- | --- |
| `MAIN-ACCESS-0030` | runtime | `access.relate` got a relation with no name, or a resource with no `id` (400). Fix: call `access.relate(team, "editor", { type: "folder", id: "f1" })` | `access` |
| `MAIN-ACCESS-0031` | runtime | `access.list` was asked for an action that has a named rule; a rule cannot be pushed into a database query, so the list is refused instead of widened (400). Fix: list through db and check each row with `access.can`, or drop the rule from this action | `access` |
| `MAIN-ADVISOR-0002` | runtime | The model's output didn't fit: a choice outside `may`, evidence not in the input, a reason over 600 characters, or not the schema; refused and logged | `advisor` |
| `MAIN-ADVISOR-0003` | runtime | The advisor stopped: budget spent, `max-per-hour` reached, or the model failed; playbooks carry on | `advisor` |
| `MAIN-ADVISOR-0004` | runtime | The input matched a secret or personal-data detector after redaction; the field was dropped before the call | `advisor` |
| `MAIN-AI-0069` | runtime | The model's driver can't call tools, so a run with `tools` can't start (501); nothing is sent | `ai-run` |
| `MAIN-ANALYTICS-0009` | runtime | The `analytics` section of `main.yaml` is invalid: an event or metric declaration that cannot be read, a bad `to`, `without-consent` or `min-group` (500) | `analytics` |
| `MAIN-APIKEY-0012` | runtime | A manage call (`rotate`, `revoke`, `get`) names a key that isn't in the current tenant (404, same body for another tenant's). Fix: list the tenant's keys to find the id | `apikeys` |
| `MAIN-APP-0001` | runtime | The API service reference app could not sync a repository with the partner (502). Fix: Look at the dead-letter queue with `jobs.dead.list()` and retry the job once the partner is back | `reference-architectures` |
| `MAIN-APP-0002` | runtime | The API service reference app's partner answered that it is not ready (503, retryable). Fix: Retry; the job backs off and lands in the dead-letter queue after three attempts | `reference-architectures` |
| `MAIN-APP-0003` | runtime | The payments checkout reference app refused an order amount that is not a positive whole number of minor units (400), or the payment provider answered without a checkout session (502). Fix: Send a positive integer in minor units; on a provider answer, retry the checkout once the provider is back | `reference-architectures` |
| `MAIN-APPS-0010` | runtime | A record would hold a field outside `mainsdk-app@1`; refused before anything is written | `app-registry` |
| `MAIN-APPS-0011` | runtime | A declared peer app is not reachable on the link: no running record, no socket, or it did not answer | `app-registry` |
| `MAIN-AUTH-0002` | runtime | The person is signed in but may not do this: no `role` the action needs, not a manage role for people, teams and invites, a non-owner touching the owner role or requiring SSO, or an SSO user nobody added while just-in-time joining is off (403). Fix: ask an owner to grant the role or to add the person | `auth` |
| `MAIN-AUTH-0003` | runtime | The session's assurance is too low: the action needs a stronger sign-in, or its admin role needs a phishing-resistant one (403). Fix: sign in with a passkey, or through SSO with phishing-resistant MFA | `auth` |
| `MAIN-AUTH-0004` | runtime | Sign-in failed (401). One body for every bad credential, password, code or passkey, and for a passkey already claimed by another account; the reason is only in the log. Fix: check the details and try again, or use another method | `auth` |
| `MAIN-AUTH-0005` | runtime | A sign-in method that `auth.methods` doesn't list was used, or domain auto-join was used while it is off (403). Fix: add the method to `auth.methods`, or set `auth.orgs.domains.auto-join: true` | `auth` |
| `MAIN-AUTH-0010` | runtime | The action needs a recent sign-in (a `fresh` window, or the last 10 minutes for sensitive changes) and the session's is older (401). Fix: ask the person to sign in again, then retry | `auth` |
| `MAIN-AUTH-0011` | runtime | `auth.key` is not set. In production the app refuses to sign sessions (500); elsewhere it uses a random key and logs a warning, so sessions end when the process does. Fix: set `auth.key: secret:AUTH_KEY` to a random 32-byte value | `auth` |
| `MAIN-AUTH-0013` | runtime | The WhatsApp sender couldn't deliver the code (502, retryable on an outage or timeout). The error holds neither the code, the number nor the provider's reply. Fix: check the sender settings and delivery log at the provider | `auth` |
| `MAIN-AUTH-0014` | runtime | A phone number isn't valid E.164 (400). Fix: use the international format with a leading +, such as `+919876543210` | `auth` |
| `MAIN-AUTH-0015` | runtime | Too many sign-in or code attempts from one source or for one account (429, with `Retry-After`). Fix: wait, then try again | `auth` |
| `MAIN-AUTH-0016` | runtime | The refresh token is missing, unknown, expired or already used (401). Fix: sign in again | `auth` |
| `MAIN-AUTH-0017` | runtime | Sign-up is closed (403), or an account with this email or phone number already exists (409). Fix: ask the app owner for an invite, or sign in instead | `auth` |
| `MAIN-AUTH-0018` | runtime | An `auth` call got input it can't use (400): a bad email, a password outside the length range, a role not under `tenancy.roles`, a team name, a domain, `expiresIn`, a device `scope`, a SCIM group, or an SSO setting. Fix: the problem names the value; correct it | `auth` |
| `MAIN-AUTH-0019` | runtime | Account recovery was tried before its waiting period ended (403, retryable, with `Retry-After`). Fix: try again after the wait; a signed-in device can cancel the recovery until then | `auth` |
| `MAIN-AUTH-0020` | runtime | The sign-in state couldn't be read or locked, so the request was refused rather than guessed (503, retryable); a warning also says production runs sessions on the `local` kv driver, which instances don't share. Fix: retry; check the `kv` and `db` drivers | `auth` |
| `MAIN-AUTH-0021` | runtime | A security event sink or an `onRisk` hook threw. It is logged and sign-in goes on. Fix: repair the sink or hook | `auth` |
| `MAIN-AUTH-0022` | runtime | The breached-password check timed out or failed; the password was assessed without it and this was logged. Fix: check that `breached.driver` is reachable | `auth` |
| `MAIN-AUTH-0023` | runtime | `whatsapp` sign-in or a phone code was used with no sender configured (500). Fix: set `auth.whatsapp.driver` in `main.yaml`, or pass `whatsapp` to `createApp` | `auth` |
| `MAIN-AUTH-0024` | runtime | Passkeys or the device flow need the origins the app is served from and `auth.passkeys.origins` is empty (500). Fix: set `auth.passkeys.origins`, such as `[https://app.example.com]` | `auth` |
| `MAIN-AUTH-0025` | runtime | No email driver is set: recovery by email fails (500) and an invite link is not sent (logged); or the invite's getqr.id link couldn't be revoked (logged, the invite is revoked anyway). Fix: pass an email driver to `createApp` | `auth` |
| `MAIN-AUTH-0026` | runtime | The DNS lookup for a domain proof failed; the proof counts as not found and this is logged. Fix: check DNS, then run `verify` again | `auth` |
| `MAIN-AUTH-0027` | runtime | A device code is unknown, used or expired (404). Fix: start again on the device to get a new code | `auth` |
| `MAIN-AUTH-0050` | runtime | Step-up required before the session is issued (401; the problem lists the methods and carries the step-up challenge) | `auth` |
| `MAIN-AUTH-0051` | runtime | Sign-in refused: the source network is blocked (403) | `auth` |
| `MAIN-AUTH-0052` | runtime | Step-up challenge wrong, expired, reused or for another sign-in; the pending sign-in is closed (401) | `auth` |
| `MAIN-AUTH-0054` | runtime | The new password is in a known breach (422) | `auth` |
| `MAIN-AUTH-0056` | runtime | The identity provider did not answer or answered with a server error or redirect (502); the message carries neither the request nor the reply | `auth` |
| `MAIN-AUTH-0060` | runtime | No such member, team, invite, passkey or device in the current tenant (404, same body for another tenant's). Fix: check the id in the current organisation | `auth` |
| `MAIN-AUTH-0061` | runtime | A team with that name already exists in the organisation (409). Fix: pick another name | `auth` |
| `MAIN-AUTH-0080` | runtime | An auth route got the wrong method, a body that isn't JSON, or a field that fails its schema (400 or 405) | `auth-client` |
| `MAIN-AUTH-0084` | runtime | A sign-in route was called for a method `auth.methods` doesn't list (404, so the list of methods isn't probed one by one) | `auth-client` |
| `MAIN-AUTH-0100` | runtime | A request to an action marked `attest: required` carried no valid App Attest or Play Integrity token, or its nonce was unknown, used or older than 60 seconds; the refusal is audited | `expo-adapter` |
| `MAIN-AUTH-0102` | runtime | A QR sign-in code was malformed, unknown, expired, already used or approved by someone who is not signed in; the reason is in the error extensions | `mobile-auth` |
| `MAIN-AUTH-0103` | runtime | A push approval was refused: too many prompts in the window (429), or no registered device could be reached (503) | `mobile-auth` |
| `MAIN-AUTH-0104` | runtime | Account recovery could not start (no valid proof) or is not ready (waiting period not over, or cancelled) | `mobile-auth` |
| `MAIN-AUTH-0105` | runtime | A trusted device was revoked, expired or never enrolled, so its session no longer authorises anything | `mobile-auth` |
| `MAIN-AUTH-0106` | runtime | A native passkey assertion named a credential that is not registered for the person | `mobile-auth` |
| `MAIN-AUTH-0200` | runtime | The approval covers a different action, amount, payee or person than the one about to run (403); the approval is voided. Fix: ask the person to approve the exact action again | `signed-approvals` |
| `MAIN-AUTH-0201` | runtime | The approval is older than its expiry, five minutes by default (403). Fix: ask the person to approve again | `signed-approvals` |
| `MAIN-AUTH-0202` | runtime | The approval was already used, never signed or never asked for (403). Fix: ask the person to approve again | `signed-approvals` |
| `MAIN-AUTH-0203` | runtime | The passkey signature does not cover this approval, the passkey is not the person's, or the person has none (401). Fix: sign again on the approval screen with the person's own passkey | `signed-approvals` |
| `MAIN-AUTH-0204` | runtime | The action needs a signed approval and none came, or the person cancelled (428). The MCP answer carries an elicitation naming what to approve. Fix: ask the person to approve, then call again with the approval id | `signed-approvals` |
| `MAIN-AUTH-0205` | runtime | A critical action needs a second, different signer and the same person signed twice (403). Fix: ask another operator to approve | `signed-approvals` |
| `MAIN-AUTH-0206` | runtime | The approval request has no action name or a bad amount or currency (400). Fix: pass `auth.confirm("payouts.send", { amount: { value, currency }, payee })` | `signed-approvals` |
| `MAIN-BACKUP-0004` | runtime | A drill was asked to restore somewhere other than scratch, or the driver landed it in production (400). Fix: run with `--to scratch` and make the driver restore into a new branch or database | `backup-restore-evidence` |
| `MAIN-BACKUP-0005` | runtime | The provider did not return backup settings (502, retryable). Fix: give the app a read-only provider token and retry | `backup-restore-evidence` |
| `MAIN-BOT-0001` | runtime | Challenge required (403; the problem's `challenge` member carries a `BotChallenge`) | `bot-defence` |
| `MAIN-BOT-0002` | runtime | Refused as automated (403; the reasons are logged, not returned) | `bot-defence` |
| `MAIN-BOT-0003` | runtime | Challenge solution wrong, expired, for another challenge, or used twice (403) | `bot-defence` |
| `MAIN-BOT-0004` | runtime | Form token missing, forged or older than 1 hour (403) | `bot-defence` |
| `MAIN-BOT-0006` | runtime | The CAPTCHA provider didn't answer within 2 s; the built-in challenge was used instead (logged once per outage) | `bot-defence` |
| `MAIN-BOT-0007` | runtime | A WhatsApp or SMS code was refused before sending: country not allowed, or pumping velocity reached (429) | `bot-defence` |
| `MAIN-CACHE-0001` | runtime | A credentialed response carried a shared-cache directive; it was replaced with `private, no-store` (logged once per route and process) | `http-cache` |
| `MAIN-CACHE-0002` | runtime | An action declared `cache.scope: public` read a session, an API key or the tenant; the response went out `private, no-store` and the error is logged | `http-cache` |
| `MAIN-CACHE-0003` | runtime | A cache key is longer than 256 characters, or a value isn't JSON or is larger than `cache.max-value` (400); nothing is stored | `http-cache` |
| `MAIN-CACHE-0004` | runtime | An app-scoped value matched a personal-data detector; it was not stored and the function's result was returned | `http-cache` |
| `MAIN-CACHE-0005` | runtime | The cache store was slower than `cache.timeout` or failed; reads and writes are skipped until it answers (logged once per outage) | `http-cache` |
| `MAIN-CANARY-0001` | runtime | A canary record was returned by a query (critical security event; the response is not changed unless `on-touch: block`) | `exfiltration-guard` |
| `MAIN-CANARY-0002` | runtime | The canary API key or canary secret was used, sent or received (critical security event) | `exfiltration-guard` |
| `MAIN-CLOUD-0001` | runtime | Cloud: the project or account is pinned to another region; the call is refused and nothing is stored here (421). The problem carries `region` and `location` | `cloud-platform` |
| `MAIN-CLOUD-0002` | runtime | Cloud: the `mainsdk-protocol` header is missing or names a wire version the Cloud does not read (400); the problem names the header | `cloud-platform` |
| `MAIN-CLOUD-0003` | runtime | Cloud: the SDK major in `mainsdk-sdk-major` is not among the current and two previous majors (426) | `cloud-platform` |
| `MAIN-CLOUD-0004` | runtime | Cloud: no ingest key or CI identity token, or one that is not valid for this project (401). A project ID is never a credential | `cloud-platform` |
| `MAIN-CLOUD-0005` | runtime | Cloud: no signed-in session, or it expired (401) | `cloud-platform` |
| `MAIN-CLOUD-0006` | runtime | Cloud: the request is not valid: a project id that is not a UUIDv7, an unknown region or environment, a bad batch or an unknown device code (422, or 404 for a code) | `cloud-platform` |
| `MAIN-CLOUD-0007` | runtime | Cloud: there is no such add-on (404) | `usage-billing` |
| `MAIN-CLOUD-0008` | runtime | Cloud: no such project or account in this account; the same answer whether it exists elsewhere or not (404) | `cloud-platform` |
| `MAIN-CLOUD-0009` | runtime | Cloud: the project id is claimed under another git remote or by another account; the CLI offers a new id (409, `claimedRemote`) | `cloud-platform` |
| `MAIN-CLOUD-0010` | runtime | Cloud: production needs a passkey or SSO with phishing-resistant MFA; a one-time code or social sign-in does not count (403, `required`) | `production-access` |
| `MAIN-CLOUD-0011` | runtime | Cloud: this action needs a fresh passkey assertion bound to it; the problem carries `stepUp` with the challenge and the prompt (403) | `production-access` |
| `MAIN-CLOUD-0012` | runtime | Cloud: sharing through getqr.id is not configured for this Cloud (503) | `cloud-platform` |
| `MAIN-CLOUD-0403` | runtime | Cloud: Free has one seat; a second member needs Teams (402, `upgrade`). A read-only getqr.id link needs no seat | `usage-billing` |
| `MAIN-CLOUD-0500` | runtime | Cloud: the Cloud could not finish the call (500). The app is never affected | `cloud-platform` |
| `MAIN-CONN-0010` | runtime | `connect` got a `returnTo` that isn't a path in this app: it must start with a single `/` (400) | `connections` |
| `MAIN-CONNECTOR-0002` | runtime | A call to the tool failed or was refused with a 4xx or 5xx; the audit log line names the operation and status (502) | `connectors` |
| `MAIN-CONTEXT-0002` | runtime | A share or receive was held: no recorded consent for the purpose on this side, or the peer reported none; nothing was sent or stored | `context` |
| `MAIN-CONTEXT-0003` | runtime | A peer isn't first-party: its seal's owner differs from this app's, or it doesn't declare the same group; refused | `context` |
| `MAIN-CONTEXT-0004` | runtime | A field or value was refused: not in the purpose's list, a string over 64 characters, a labelled `personal`, `sensitive`, `child` or `phi` field, or a value matching a secret, email, phone or card detector | `context` |
| `MAIN-CONTEXT-0005` | runtime | A cross-tenant flow was refused: the receiving context id isn't in the sender's tenant | `context` |
| `MAIN-CONTEXT-0007` | runtime | An erasure couldn't reach a peer; it was queued and retried, and the receipt lists the peer as pending | `context` |
| `MAIN-CRYPTO-0001` | runtime | A `crypto` input is the wrong shape (400): a key that isn't 32 bytes, an HMAC key under 16 bytes, a token under 16 bytes, random bytes outside 1 to 65536, bad base64url, or an ed25519 key that doesn't import. Fix: pass the size or format the problem names, for example `crypto.generateKey()` | `keys` |
| `MAIN-CRYPTO-0003` | runtime | A sealed box couldn't be opened (400): too short, a wrong key, changed bytes or a different `aad`. Fix: pass the whole value `crypto.seal` returned, with the key and `aad` that sealed it | `keys` |
| `MAIN-CRYPTO-0010` | runtime | The platform has no implementation of the requested post-quantum algorithm (for example SLH-DSA, or ML-KEM on Bun); nothing is substituted silently | `post-quantum` |
| `MAIN-CRYPTO-0011` | runtime | FIPS mode is on and the call asked for an algorithm that is not approved | `post-quantum` |
| `MAIN-CRYPTO-0012` | runtime | FIPS mode is on and this platform has no FIPS-validated crypto provider | `post-quantum` |
| `MAIN-DATA-0064` | runtime | The outbox couldn't relay an entry after its last retry; the entry moved to the job dead-letter queue | `db-writes` |
| `MAIN-DEPLOY-0006` | runtime | A tick arrived without the bearer secret, or with the wrong one (401); nothing ran | `deploy` |
| `MAIN-DEPLOY-0007` | runtime | A tick named a schedule the app doesn't declare (404) | `deploy` |
| `MAIN-DEPLOY-0008` | runtime | A tick arrived later than `deploy.tick.late` after its slot; it ran, and the delay is logged | `deploy` |
| `MAIN-DEVICE-0005` | runtime | An envelope was refused: payload not in JCS form, unknown payload type, bad signature, unknown key id, `aud` not this app (401) | `device-identity` |
| `MAIN-DEVICE-0006` | runtime | A request was not fresh: `iat` more than 10 minutes from server time, or the nonce was seen in the last 20 minutes (401, with `serverTime`) | `device-identity` |
| `MAIN-DEVICE-0007` | runtime | An enrolment proof was refused: the token is unknown, expired, used up or revoked; the code wasn't approved or expired; the OIDC token doesn't match a declared issuer, audience and subject (403). Evidence below a required level is `MAIN-DEVICE-0023` | `device-identity` |
| `MAIN-DEVICE-0008` | runtime | The device is revoked; every request from it is refused (403, with a signed `revoked` object) | `device-identity` |
| `MAIN-DEVICE-0009` | runtime | The credential expired more than `renew-grace` ago; the device must enrol again (401) | `device-identity` |
| `MAIN-DEVICE-0010` | runtime | A rotation was refused: a signature from the old or new key is missing, `prev` doesn't match the current credential, or the new key was used before (409) | `device-identity` |
| `MAIN-DEVICE-0011` | runtime | The tenant has `device.max` devices that aren't revoked; enrolment refused (403) | `device-identity` |
| `MAIN-DEVICE-0012` | runtime | The device API is busy or its store doesn't answer; try later (503, `Retry-After`) | `device-identity` |
| `MAIN-DEVICE-0013` | runtime | The device belongs to another region; the response names the `base` to use (421) | `device-identity` |
| `MAIN-DEVICE-0014` | runtime | A `device.*` call was refused: no tenant in context, or no `device.manage` or `device.view` permission (403) | `device-identity` |
| `MAIN-DEVICE-0015` | runtime | A client call was refused: `enrol` on an enrolled client without first deleting its key, or a key store that can't be read or written | `device-identity` |
| `MAIN-DEVICE-0017` | runtime | Too many enrolment attempts for one token or one address; try later (429, `Retry-After`) | `device-identity` |
| `MAIN-DEVICE-0018` | runtime | The device or enrolment doesn't exist in the caller's tenant; a device of another tenant is reported the same way (404) | `device-identity` |
| `MAIN-DEVICE-0020` | runtime | Evidence was refused: format not accepted, binding or nonce wrong or older than 60 seconds, a chain to a root the tenant doesn't accept, failed boot verification, a security level under `min-security-level`, or a measurement not in `builds`. The device gets the code only; the server log names the check (403) | `device-attest` |
| `MAIN-DEVICE-0021` | runtime | A device missed its re-attestation past `grace`; its level dropped to `none` (logged, `device.attestation.failed` signal) | `device-attest` |
| `MAIN-DEVICE-0023` | runtime | An enrolment, a renewal, a policy target or a command needs a higher attestation level than the device holds (403, with `need` and `have`) | `device-attest` |
| `MAIN-DEVICE-0024` | runtime | The device's TCB is `out-of-date` past its grace or `revoked`; renewal refused until it re-attests on a fixed platform (403) | `device-attest` |
| `MAIN-DEVICE-0030` | runtime | `client.send` refused a record: the stream isn't declared in the client or by the server, the record isn't a JSON object, it is over `max-record`, or it fails the stream's schema on the device | `device-telemetry` |
| `MAIN-DEVICE-0031` | runtime | A batch is over `max-batch` or holds more than 1,000 records (413) | `device-telemetry` |
| `MAIN-DEVICE-0032` | runtime | A batch is out of order: `seq` isn't the next one, or `prev` doesn't match the last accepted batch (409, with `expect`) | `device-telemetry` |
| `MAIN-DEVICE-0033` | runtime | A record was rejected: unknown stream, over its stream's `max-record`, or it fails the stream's schema on the server | `device-telemetry` |
| `MAIN-DEVICE-0034` | runtime | A record was rejected because the detectors found a secret, or personal data in a stream with `personal: false` | `device-telemetry` |
| `MAIN-DEVICE-0035` | runtime | Telemetry is rate limited: the device's `rate` or the tenant's `daily` bytes is reached (429, `Retry-After`) | `device-telemetry` |
| `MAIN-DEVICE-0036` | runtime | A handler threw or took longer than 10 seconds; the batch wasn't acknowledged and will come again (503) | `device-telemetry` |
| `MAIN-DEVICE-0037` | runtime | The client's buffer is full or holds records older than `queue.age`; the oldest were dropped (logged once an hour with counts per stream) | `device-telemetry` |
| `MAIN-DEVICE-0045` | runtime | A publish was refused: no `device.policy.define`, content that fails the schema, an unknown group, or `minClient` that isn't a version (400) | `device-policy` |
| `MAIN-DEVICE-0046` | runtime | An overlay was refused: its path isn't under `tighten`, or the value wouldn't tighten the value in force (400) | `device-policy` |
| `MAIN-DEVICE-0047` | runtime | The device refused a bundle: bad signature, wrong `aud` or tenant, a version not above the one it holds, an overlay that loosens, or a `minClient` above its own; it kept its last good policy and reported why | `device-policy` |
| `MAIN-DEVICE-0048` | runtime | The device's apply handler threw; it kept its last good policy and reported `failed` with the error code | `device-policy` |
| `MAIN-DEVICE-0049` | runtime | A rollout stopped: the failed share passed `rollback-if`; the previous content was published again as a new version (logged, audited, `device.policy.rolledback` signal) | `device-policy` |
| `MAIN-DEVICE-0051` | runtime | The publish waits for approval; it applies when the people `approve` asks for have approved (202) | `device-policy` |
| `MAIN-DEVICE-0060` | runtime | A create was refused: the type isn't defined, the input fails its schema, the target is empty, or `expiresIn` is over `max-ttl` (400) | `device-commands` |
| `MAIN-DEVICE-0061` | runtime | The create targets more than `max-targets` devices without `confirm` equal to the count (409, with `count`) | `device-commands` |
| `MAIN-DEVICE-0062` | runtime | The command waits for approval (202); an app agent's approval was refused | `device-commands` |
| `MAIN-DEVICE-0063` | runtime | The device refused a command: bad signature, wrong `aud`, tenant or device, expired, an id it already ran, a type it hasn't registered, a risk that differs from its handler's, or fewer approvals than its floor; the receipt says `refused` | `device-commands` |
| `MAIN-DEVICE-0064` | runtime | The command expired before it was delivered or before a receipt came back | `device-commands` |
| `MAIN-DEVICE-0065` | runtime | The handler threw, timed out or returned a result that fails its schema; the receipt says `failed` | `device-commands` |
| `MAIN-DEVICE-0068` | runtime | A cancel or revert was refused: the command was already delivered, its type has no `undo`, or it isn't `done` (409) | `device-commands` |
| `MAIN-DEVICE-0069` | runtime | A receipt was refused: bad signature, an unknown command, or a digest that doesn't match the command sent (400) | `device-commands` |
| `MAIN-DEVICE-0070` | runtime | The device's command-log head differs from the one the server computes from its receipts (security event `device.command_log_mismatch`, high) | `device-commands` |
| `MAIN-DEVICE-0080` | runtime | A heartbeat was refused: its `seq` isn't above the last one (401) | `device-health` |
| `MAIN-DEVICE-0081` | runtime | The device's clock is more than 5 minutes off the app's; the offset is recorded and corrected for (logged once a day per device) | `device-health` |
| `MAIN-DEVICE-0083` | runtime | A device went silent (logged at info, `device.silent` signal) | `device-health` |
| `MAIN-DEVICE-0090` | runtime | The status list couldn't be refreshed; the last list is used until its `validUntil`, after which third parties treat credentials as unverifiable, while the app's own checks go on (logged once per outage) | `device-taps` |
| `MAIN-DEVICE-0091` | runtime | A delegation chain or approval decision on a command or publish is invalid: a broken chain, authority that widens down the chain, a revoked or expired link, or an approval by a key not in the tenant's DID document; the server refuses to send it and a device refuses to run it | `device-taps` |
| `MAIN-DEVICE-0093` | runtime | An agent passport was refused: an operator not listed, a passport not countersigned by the tenant, or a heartbeat reporting a model or version the passport doesn't name (TAPS-ATTEST §3: a change needs a new passport) | `device-taps` |
| `MAIN-DRIVER-0003` | runtime | Only verified developers can rate a driver (403). Fix: verify your developer record, then rate the driver | `driver-marketplace` |
| `MAIN-DRIVER-0004` | runtime | The developer has no project that runs the driver (403). Fix: rate a driver after your project runs it in production | `driver-marketplace` |
| `MAIN-DRIVER-0005` | runtime | A rating is not a whole number from 1 to 5 (400). Fix: send score as 1, 2, 3, 4 or 5 | `driver-marketplace` |
| `MAIN-E2E-0001` | runtime | `e2e` was used before `configureE2e` (500) | `e2e` |
| `MAIN-E2E-0002` | runtime | No such group on this device (404) | `e2e` |
| `MAIN-E2E-0003` | runtime | The request is invalid: a bad id, more than 1000 members, data over 4 MiB, a member with no key package, a member already in the group, or removing yourself (400, 413 for size) | `e2e` |
| `MAIN-E2E-0004` | runtime | The message can't be decrypted: this device holds no key for its epoch (a removed member, or a member added later), it was changed, or it belongs to another group (403, 400 for a value that is not a sealed message) | `e2e` |
| `MAIN-E2E-0005` | runtime | The passkey gave no PRF output (400) | `e2e` |
| `MAIN-E2E-0006` | runtime | Nothing to recover with this passkey: no recovery blob, or a different passkey (404) | `e2e` |
| `MAIN-E2E-0010` | runtime | The call asks for a suite or feature the driver does not implement (501) | `e2e` |
| `MAIN-E2E-0011` | runtime | The caller is not a member of the group, so it can't add, remove or encrypt (403) | `e2e` |
| `MAIN-E2E-0012` | runtime | A commit conflicted with another member's commit for the same epoch and the retries are used up (409) | `e2e` |
| `MAIN-EMAIL-0002` | runtime | The recipient is not a single address, or a header or the subject holds a line break (400) | `email-notify` |
| `MAIN-EMAIL-0003` | runtime | The template does not exist, or a placeholder has no value in `data` (400) | `email-notify` |
| `MAIN-EMAIL-0004` | runtime | The provider could not send the email; `retryable` says whether a retry or a fallback driver may help (502) | `email-notify` |
| `MAIN-EMAIL-0005` | runtime | The tenant's per-minute or per-day send limit is reached (429, `retryAfter`) | `email-notify` |
| `MAIN-EMAIL-0007` | runtime | The unsubscribe link was tampered with or is not valid (400) | `email-notify` |
| `MAIN-EMAIL-0008` | runtime | `email.driver: local` in production; the app refuses to start (500) | `email-notify` |
| `MAIN-EVENT-0001` | runtime | `events.publish` got a type that isn't lowercase words joined by dots, or data that isn't JSON (400); the outbox refuses a non-JSON event too. Fix: use a type such as `order.shipped` and plain JSON data | `webhooks-jobs` |
| `MAIN-EVENT-0014` | runtime | A handler or tap on `events.on` threw. It is logged and the other handlers still run. Fix: repair the handler; the event is not retried in process, so use `jobs` for work that must finish | `webhooks-jobs` |
| `MAIN-EXFIL-0001` | runtime | An outbound call carried a resolved secret to a host other than that secret's driver hosts (blocked in enforce) | `exfiltration-guard` |
| `MAIN-EXFIL-0002` | runtime | An outbound call carried labelled values read in this request to a host not declared as a receiver for that label (blocked in enforce) | `exfiltration-guard` |
| `MAIN-EXFIL-0003` | runtime | A response held: more records with labelled fields than the action's `bulk` allows for one response, or than `actor-window` allows for one actor over the window (403) | `exfiltration-guard` |
| `MAIN-EXFIL-0004` | runtime | A response carried a resolved secret or a card number; replaced with a 500 `MAIN-CORE-0500` problem | `exfiltration-guard` |
| `MAIN-EXFIL-0005` | runtime | A body was larger than `scan.max`; the rest was not scanned (logged once per route a minute) | `exfiltration-guard` |
| `MAIN-FEED-0009` | runtime | A feeds argument or setting is invalid: a trending key outside `^[a-z0-9:_.-]{1,64}$`, a bad `until`, `order`, `visibility` or limit (400) | `feeds` |
| `MAIN-FLEET-0001` | runtime | A fleet policy has an unknown key or is not plain data (400). Fix: send declarative JSON policy only, never code | `fleet-control` |
| `MAIN-FLEET-0002` | runtime | A policy or push is not signed by the tenant role with enough tenant keys; MainSDK's own key does not count (403). Fix: sign with the tenant's keys up to the role threshold | `fleet-control` |
| `MAIN-FLEET-0003` | runtime | A kill or response action is malformed, or lacks a second operator's approval and is not a logged break-glass (400 or 403). Fix: name the target and get a second signature, or use break-glass with a reason | `fleet-control` |
| `MAIN-FLEET-0004` | runtime | Egress refused personal data going to a region the policy forbids (403). Fix: send it to an allowed region or change the egress policy | `fleet-control` |
| `MAIN-FLEET-0005` | runtime | A push does not exist or was already confirmed (404 or 409). Fix: preview a new push | `fleet-control` |
| `MAIN-FLEET-0007` | runtime | The deploy gate refused a release that lacks a required mark (403). Fix: run the missing step, then deploy again | `fleet-control` |
| `MAIN-FLEET-0008` | runtime | A provider is switched off by a fleet kill switch and declares no fallback (503). Fix: wait for the kill to lift or declare a `fallback` | `fleet-control` |
| `MAIN-FLEET-0009` | runtime | Fleet actions are not active for this tenant: no Fleet contract and no incident mode (402). Fix: activate incident mode or sign a Fleet contract | `fleet-defend` |
| `MAIN-FLEET-0010` | runtime | A build includes a package the threat feed marks compromised, and the intel mode is block (403). Fix: update the package, or set the intel start mode to warn | `fleet-defend` |
| `MAIN-FLEET-0011` | runtime | An evidence bundle was asked for a standard Fleet does not map (400). Fix: use one of the listed standards | `fleet-prove` |
| `MAIN-FLEET-0012` | runtime | An enrolment or report was refused: no valid proof from device management, a registered workload identity, the tenant's DNS or a vendor consent, or the report is not signed by the app's key (403). Fix: present proof from a channel the tenant controls; the app runs normally without Fleet | `fleet-enrolment` |
| `MAIN-FLEET-0013` | runtime | The free Fleet view was opened for an organisation that has not proved its domain (403). Fix: add the TXT record named in the error, then open the view again | `fleet-enrolment` |
| `MAIN-FLEET-0014` | runtime | A signed report names hosts that the build provenance does not, so the report is flagged and the app's mark shows as unverified (flag, not a refusal). Fix: rebuild with provenance that lists the hosts, or remove the host | `fleet-enrolment` |
| `MAIN-FLEET-0015` | runtime | A surface was exported to a format Fleet does not write (400). Fix: use k8s-networkpolicy, vercel-firewall or aws-sg | `fleet-discover` |
| `MAIN-FLEET-0016` | runtime | A package query is not `name@version` (400). Fix: write the query as name@version, such as lodash@4.17.20 | `fleet-observe` |
| `MAIN-GEO-0001` | runtime | A `GeoHint` was passed into a `policy`, `access` or `auth.require` context (400) | `geo` |
| `MAIN-GEO-0003` | runtime | `geo.geocode` was called with an empty address or one over 512 characters, or the driver has no geocoder (`platform-headers`) (400) | `geo` |
| `MAIN-GEO-0004` | runtime | The geocoding driver has no host that serves the tenant's region, so the address is not sent (400) | `geo` |
| `MAIN-GEO-0006` | runtime | A geocoding provider could not answer: down, refused, slow past `geo.timeout` or unreadable (502). `geo.geocode` catches it, logs one line without the address and returns `null` | `geo` |
| `MAIN-GUARD-0003` | runtime | A blast-radius ceiling refused a step: `max-active`, `block.max-ranges`, a range wider than `min-prefix` or in `never`, a limit under `limit-floor`, or a protected target | `reaction-safety` |
| `MAIN-GUARD-0004` | runtime | A loop was stopped: the hop limit, the trigger's cause chain already holds this rule's own run, or a neighbour pair cooldown | `reaction-safety` |
| `MAIN-GUARD-0006` | runtime | `max-per-hour` reached; further runs are `suppressed` and counted, with one alert for the hour | `reaction-safety` |
| `MAIN-GUARD-0007` | runtime | A rule is quarantined after a rollback as worse; matches are `suppressed` until the quarantine ends | `reaction-safety` |
| `MAIN-HOOK-0001` | runtime | A webhook call got input it can't use (400): a non-JSON payload, or a bad argument to `webhooks.*`; or `webhooks.emit` ran with no signing secret (500). Also the code logged for a delivery attempt that failed with no more specific code. Fix: pass plain JSON, and set `webhooks.secret: secret:WEBHOOK_SIGNING_KEY` | `webhooks-jobs` |
| `MAIN-I18N-0010` | runtime | No pending draft with that id (404). Fix: list drafts with `i18n.drafts.list()` and use the id of a pending one | `i18n` |
| `MAIN-I18N-0011` | runtime | A draft or an export or import request can't run (400): drafting without the `ai` interface, or an export or import without exactly one target locale and a known format. Fix: configure `ai` and `i18n.ai-draft.model`, or pass `--locale` and a format of `xliff`, `xcstrings`, `android` or `icu-json` | `i18n` |
| `MAIN-I18N-0012` | runtime | A translation provider answered with an error (503, retryable). The app keeps serving the last good messages. Fix: check the provider token and project id | `i18n` |
| `MAIN-IDEM-0001` | runtime | Another caller is still running the same idempotent call and holds its key; the wait ran out (409, retryable). Fix: retry shortly; the first call hasn't finished | `webhooks-jobs` |
| `MAIN-IDEM-0002` | runtime | An idempotency record couldn't be kept, or one outside a tenant is held only in process (logged). A repeat may run the work again, or a duplicate is caught per instance only. Fix: check the idempotency store, and run the call inside `tenant.run` | `webhooks-jobs` |
| `MAIN-IDENT-0002` | runtime | A wallet presentation was refused: bad signature, untrusted issuer, wrong nonce or audience, missing key binding, altered after binding, or a requested claim not disclosed | `digital-ids` |
| `MAIN-IDENT-0003` | runtime | An agent approval (CIBA) was refused: unknown, expired or already answered request, a denial, or a token that exceeds the 5 minute cap or the action scope | `digital-ids` |
| `MAIN-IDENT-0004` | runtime | DigiLocker could not verify the person: the token or user call failed, or the response was not usable | `digital-ids` |
| `MAIN-JOB-0001` | runtime | `jobs.run` or `jobs.schedule` got input it can't use (400): a bad name, an input that isn't JSON or is too large, or an option out of range. Fix: pass plain JSON and the documented options | `webhooks-jobs` |
| `MAIN-JOB-0002` | runtime | A job attempt threw, or the job ran out of attempts after its worker stopped (logged per attempt). After the last attempt the job is `dead` in the dead-letter queue, or `failed` when `dead-letter: false` (500). Fix: read the job error, fix the handler and retry the job | `webhooks-jobs` |
| `MAIN-JOB-0003` | runtime | A job was claimed whose name has no registered handler (500); it is retried and then dead-lettered. Fix: register a handler for the job name before the worker starts | `webhooks-jobs` |
| `MAIN-JOB-0004` | runtime | A cron schedule isn't five valid fields (400). Fix: write five fields, such as `0 3 * * *` for 03:00 UTC every day | `webhooks-jobs` |
| `MAIN-JOB-0005` | runtime | `jobs.dead.retry` names no dead job in the current tenant (404). Fix: list the dead-letter queue with `jobs.dead.list()` | `webhooks-jobs` |
| `MAIN-JOB-0006` | runtime | The job queue can't be reached (503, retryable). Nothing was queued or claimed. Fix: check the database the `jobs` driver uses, then retry | `webhooks-jobs` |
| `MAIN-KEYS-0002` | runtime | A data label is refused (400): `pan`, because card numbers are never stored, or one that isn't `personal`, `sensitive`, `child` or `phi`. Fix: pass a canonical label, and hand card data to `pay` | `keys` |
| `MAIN-KEYS-0003` | runtime | `keys` got a call or setup it can't use (400, or 500 for a local master secret under 16 characters): a region that isn't `us`, `eu` or `in`, no KMS key for the region, a record pinned to another region, a `decrypt` with no `purpose`, a driver given no client, or rotation on a driver that rotates itself. Fix: add `keys.key.<region>` to `main.yaml`, pass a `purpose`, or use the setting the problem names | `keys` |
| `MAIN-KEYS-0005` | runtime | The key service didn't answer, or answered with nothing usable (502, retryable). Fix: check the KMS is reachable and the app may use the key, then retry | `keys` |
| `MAIN-KEYS-0006` | runtime | A sealed value, wrapped key or encrypted field failed its integrity check, or isn't a value `keys.encrypt` made (400): it was changed, moved to another row or field, or sealed under other keys. Fix: use the keys and context it was sealed with, or restore the value from a backup | `keys` |
| `MAIN-KEYS-0020` | runtime | The HSM or KMIP server has no key with that label or version (404). Fix: create the key, turn on provisioning, or restore the version | `hsm-keys` |
| `MAIN-KEYS-0022` | runtime | An HSM wrapping key is extractable or not sensitive (500). Fix: create it with `CKA_EXTRACTABLE` false and `CKA_SENSITIVE` true | `hsm-keys` |
| `MAIN-KEYS-0024` | runtime | The KMIP server refused an operation or sent a malformed message (502). Fix: give the app permission to use the key and check the endpoint | `hsm-keys` |
| `MAIN-KEYS-0025` | runtime | The customer disabled or revoked its key, so that org's data cannot be read (403). Fix: ask the customer to enable the key; other orgs are unaffected | `hsm-keys` |
| `MAIN-MCP-0011` | runtime | A token exchange was refused: no person or agent, no scope, a scope outside `mcp.auth.scopes`, or a `token-ttl` over 10 minutes (400) | `mcp-actions` |
| `MAIN-MOBILE-0002` | runtime | A write to plain storage (AsyncStorage) used a key that names a secret such as a token or refresh credential; secrets belong in the device vault (Keychain or Keystore) | `expo-adapter` |
| `MAIN-NEIGHBOUR-0002` | runtime | The host key or signal folder is unsafe (world-writable, owned by another user and not root) or unreadable; the host bus stays off and the app keeps serving | `neighbours` |
| `MAIN-NEIGHBOUR-0003` | runtime | A host envelope failed the host-key check, or claimed a peer's identity and failed its ed25519 check; dropped and counted | `neighbours` |
| `MAIN-NEIGHBOUR-0004` | runtime | An ask was refused: the asker isn't a mutual peer, the verb isn't in `asks.accept`, the asked posture is above `max-posture`, `for` is over `max-for`, or the peer passed `max-per-hour` | `neighbours` |
| `MAIN-NEIGHBOUR-0006` | runtime | A neighbour passed `max-per-minute`; its host signals were dropped (one record a minute with the count) | `neighbours` |
| `MAIN-NEIGHBOUR-0007` | runtime | A signal outside `share` was asked to go to the host; it stayed in the app | `neighbours` |
| `MAIN-NET-0021` | runtime | `net.listen`, `net.listenUnix` or a UDP bind on a port, address or path that `network.listen` doesn't declare, or beyond loopback without a `bind` in the entry (403 in enforce mode) | `net-sockets` |
| `MAIN-NET-0022` | runtime | A TCP connection was closed after `idle` with no bytes either way | `net-sockets` |
| `MAIN-NET-0023` | runtime | An open TCP connection failed: reset by the peer, or a write to a closed peer; never retried | `net-sockets` |
| `MAIN-NET-0024` | runtime | A TLS handshake failed: the certificate didn't verify against the roots or the name, the version is below the minimum, or a client certificate was refused or missing (502; names which; never retried) | `net-sockets` |
| `MAIN-NET-0026` | runtime | An inbound TCP connection or datagram from a peer the rules don't allow was dropped, or a UDP receive queue was full (logged at most once a minute per source, with a count) | `net-sockets` |
| `MAIN-NET-0027` | runtime | A UDP send was refused: over 65,507 bytes, or a broadcast or multicast that `network.udp` doesn't name | `net-sockets` |
| `MAIN-NET-0031` | runtime | A `net.listen` server was at `max-connections`; the new connection was closed (logged at most once a minute, with a count) | `net-sockets` |
| `MAIN-NET-0032` | runtime | A socket call got a bad argument: a port outside 0 to 65535, a unix path that isn't absolute or is over 104 bytes, data that isn't a `Uint8Array`, or a write after close (400) | `net-sockets` |
| `MAIN-NET-0033` | runtime | `net.listenUnix` found the socket's directory open to group or others, owned by another user or a symlink, or a file that isn't a socket in the way (500; nothing is bound, and the file is never replaced) | `net-sockets` |
| `MAIN-NORMS-0002` | runtime | A metric's source isn't available (no sampling on serverless, a spec not built); the metric is skipped (logged once) | `norms` |
| `MAIN-NORMS-0003` | runtime | The stored model is from another format version or is corrupt; it was discarded and learning restarted (logged once) | `norms` |
| `MAIN-NORMS-0004` | runtime | `norms.delete` refused: the caller lacks `norms.manage` (403) | `norms` |
| `MAIN-NOTIFY-0002` | runtime | The call is malformed: no person id, template or category, a missing data value, or a broadcast with no recipients (400) | `email-notify` |
| `MAIN-NOTIFY-0005` | runtime | SMS skipped: an India number with no DLT-registered template id; recorded in `skipped` | `email-notify` |
| `MAIN-NOTIFY-0007` | runtime | The channel provider could not deliver the message; the next channel is tried (502) | `email-notify` |
| `MAIN-NOTIFY-0008` | runtime | Push skipped: the payload would carry personal data; recorded in `skipped` | `email-notify` |
| `MAIN-NOTIFY-0009` | runtime | The broadcast's rollout state is missing, already finished or cannot move (409) | `email-notify` |
| `MAIN-NOTIFY-0010` | runtime | Channel skipped: the daily SMS or WhatsApp cap is reached; recorded in `skipped` | `email-notify` |
| `MAIN-NOTIFY-0011` | runtime | `notify.sms` or `notify.whatsapp` is a local channel in production; the app refuses to start (500) | `email-notify` |
| `MAIN-OPS-0002` | runtime | A connection to the ops socket or HTTP endpoint had no token or a wrong one; refused and counted | `ops-mcp` |
| `MAIN-OPS-0003` | runtime | An HTTP request had a non-loopback `Host` or `Origin`, or arrived on a non-loopback address; refused (DNS rebinding guard) | `ops-mcp` |
| `MAIN-OPS-0004` | runtime | A write tool was called with `ops.mcp.write: off`, or a tool name outside the table | `ops-mcp` |
| `MAIN-OPS-0006` | runtime | The ops agent's budget is spent; tools answer with this code until the period resets | `ops-mcp` |
| `MAIN-PACK-0016` | runtime | A write of a region-locked field to a store outside the region of the app's markets (403). Fix: write the data to a store in the region of your markets | `industry-packs` |
| `MAIN-PACK-0017` | runtime | An analytics or ad call for a child without verified parental consent (403). Fix: collect verifiable parental consent first, or do not track this person | `industry-packs` |
| `MAIN-PAY-0033` | runtime | `pay.driver` names a driver that isn't built yet; retired, an unbuilt driver now raises `MAIN-SDK-0003`. Fix: Use `local` or `stripe` until it ships | `pay` |
| `MAIN-PERM-0006` | runtime | A write would take a folder past its `max` (507 Insufficient Storage) | `app-permissions` |
| `MAIN-PERM-0007` | runtime | A write was refused because the filesystem's free space is under `permissions.disk.reserve` (507); logged once per change | `app-permissions` |
| `MAIN-PILOT-0001` | runtime | The organisation has not published its DNS proof record; the problem detail shows the record to add (403) | `fleet-pilots` |
| `MAIN-PILOT-0002` | runtime | A pilot request is not valid: a missing or malformed organisation, region, date or id (400) | `fleet-pilots` |
| `MAIN-PILOT-0003` | runtime | The project is not owned by the tenant's verified organisation (403) | `fleet-pilots` |
| `MAIN-PILOT-0004` | runtime | A contract can't be activated: the SOC 2 Type I report is missing or out of date, or a Fleet contract has no Type II report (409) | `fleet-pilots` |
| `MAIN-PILOT-0005` | runtime | A delegated role is invalid: no keys, a threshold out of range, or a MainSDK key in the role (400) | `fleet-pilots` |
| `MAIN-PILOT-0006` | runtime | Only MainSDK staff may run pilot commands (403) | `fleet-pilots` |
| `MAIN-PILOT-0007` | runtime | No pilot tenant with that id (404) | `fleet-pilots` |
| `MAIN-PILOT-0008` | runtime | MainSDK Cloud could not issue the pilot licence: no issuer is configured, or the service refused (503 or 502) | `fleet-pilots` |
| `MAIN-PLAN-0011` | runtime | A plan or grant the call names doesn't exist (404), or an entitlements call got arguments it can't use (400). Fix: use a plan declared under `plans`, or list the tenant's grants for the id | `entitlements` |
| `MAIN-PLAT-0003` | runtime | A collaboration call was refused because the user may not read or write the document; nothing is sent to them | `platforms` |
| `MAIN-PLAYBOOK-0004` | runtime | A run started, planned, waited, applied, rolled back, expired or was declined; the record carries the run | `playbooks` |
| `MAIN-PLAYBOOK-0005` | runtime | A step failed while applying; earlier steps of the run were rolled back | `playbooks` |
| `MAIN-PLAYBOOK-0006` | runtime | A run was rolled back because errors or latency got worse after it (`rollback-if`); the rule is quarantined (spec reaction-safety) | `playbooks` |
| `MAIN-PLAYBOOK-0007` | runtime | An approval for a run was not answered within `approve-timeout` (default 30m); the run ended as `declined` | `playbooks` |
| `MAIN-PLAYBOOK-0008` | runtime | `playbooks.revert` or `playbooks.run` refused: the caller lacks `playbooks.manage` or a fresh phishing-resistant session (403) | `playbooks` |
| `MAIN-PLAYBOOK-0009` | runtime | The runtime store didn't answer; app-scope overlays applied on this instance only (logged once a minute) | `playbooks` |
| `MAIN-POLICY-0001` | runtime (load) | A Cedar or Rego policy has a syntax error; the message names the line | `policy` |
| `MAIN-POLICY-0002` | runtime | `policy.explain` was given a decision id that is not remembered | `policy` |
| `MAIN-POLICY-0003` | runtime | A decision input lacks its subject, action or resource | `policy` |
| `MAIN-POLICY-0005` | runtime (load) | A rollout's stages are not `shadow` or percentages from 0 to 100, lowest first | `policy` |
| `MAIN-POLICY-0006` | runtime | The browser client's server route did not answer or refused a decision; the action stays denied | `policy` |
| `MAIN-POLICY-0012` | runtime (load) | A Rego rule uses a construct the in-process subset does not support; it names the construct and line | `policy` |
| `MAIN-PRIV-0002` | runtime | A privacy call got a missing or malformed argument: a user id, a reason, a purpose name, an until date or a consent signal object (400) | `privacy-lifecycle` |
| `MAIN-PRIV-0003` | runtime | The person is under a legal hold, so `privacy.erase` is refused (423) | `privacy-lifecycle` |
| `MAIN-PRIV-0005` | runtime | A consent string is not a TCF v2 or GPP string this module reads (400) | `privacy-lifecycle` |
| `MAIN-PRIV-0006` | runtime | An erasure is partial: a driver has not confirmed after its retries (logged as a warning, never thrown) | `privacy-lifecycle` |
| `MAIN-REALTIME-0018` | runtime | Subscribe or upgrade refused because the app is draining (503) | `realtime` |
| `MAIN-REPORT-0001` | runtime | An error group is new in this release (warning record; raises the `errors` alert source) | `error-reporting` |
| `MAIN-REPORT-0002` | runtime | An error group passed `alert.spike` in its window (raises the `errors` alert source) | `error-reporting` |
| `MAIN-REPORT-0003` | runtime | The error driver couldn't deliver; events are dropped with a count and never block (logged once per outage) | `error-reporting` |
| `MAIN-REPORT-0004` | runtime | A browser report was refused: `browser` is off (404), the body is over 16 KB (413), or the sender passed the rate limit (429) | `error-reporting` |
| `MAIN-REPORT-0006` | runtime | The process hit an uncaught exception or unhandled rejection; it was captured as `fatal`, flushed for at most 2 s, and the process exits with code 1 | `error-reporting` |
| `MAIN-REPORT-0007` | runtime | `log.errors.get` found no group with that fingerprint (404) | `error-reporting` |
| `MAIN-SANITIZE-0001` | runtime | Input over `maxBytes`, or nested deeper than 100 elements (400; nothing is returned) | `sanitize` |
| `MAIN-SANITIZE-0002` | runtime | An option asks for a tag, attribute or URL scheme that is never allowed (`script`, `on*`, `javascript:`); thrown when the options are first used | `sanitize` |
| `MAIN-SDK-0003` | runtime | The driver named in `main.yaml` isn't built or installed for this interface; `fix` names the drivers that are. Replaces `MAIN-FILES-0014`, `MAIN-AI-0013`, `MAIN-PAY-0033` and that use of `MAIN-CONFIG-0003` | `cohesion` |
| `MAIN-SDK-0004` | runtime | The runtime store didn't answer within `runtime.store.timeout`; the caller's `on-store-down` policy was applied (ADR-0050) | `cohesion` |
| `MAIN-SDK-0005` | runtime | A call needs a context value (user, agent or region) that isn't set; `fix` names the adapter or `context.run` | `cohesion` |
| `MAIN-SDK-0006` | runtime | A duration, size, rate or money value doesn't parse; `fix` gives the accepted forms. Replaces the other uses of `MAIN-CONFIG-0003` and `MAIN-NET-0012` | `cohesion` |
| `MAIN-SDK-0007` | runtime | A connection to a backing service (the RESP client behind Redis) closed, timed out, was aborted or the server refused a command; `retryable` is true for the connection cases and `fix` names the host to declare. Replaces the plain `Error` throws in `core/resp.ts` | `cohesion` |
| `MAIN-SECRET-0006` | runtime | A secret name that is not upper-case letters, digits and underscores, or a `secrets` block missing a setting its driver needs | `config-secrets` |
| `MAIN-SECRET-0008` | runtime | The secrets provider is unreachable and no earlier value is held, so the read cannot be served | `config-secrets` |
| `MAIN-SECRET-0009` | runtime | The secrets provider refused the app's identity (a 401 or 403) | `config-secrets` |
| `MAIN-SECRET-0010` | runtime | A secret is past 80% of its `max-age` (warn, once a day per secret) | `secret-rotation` |
| `MAIN-SECRET-0012` | runtime | `mainsdk secrets rotate` on a secret without `generate`, or on a driver without `write`; prints the provider steps instead | `secret-rotation` |
| `MAIN-SECRET-0013` | runtime | A secret watcher threw; the others still ran | `secret-rotation` |
| `MAIN-SECURITY-0003` | runtime | The export buffer was full; events below `high` were dropped (one record per minute with the count) | `security-events` |
| `MAIN-SECURITY-0004` | runtime | A `security.on` subscriber threw; the others still ran | `security-events` |
| `MAIN-SECURITY-0005` | runtime | `security.emit` called with a type outside `app.*`, or with an attribute not in `telemetry@1` (the type is refused; the attribute is dropped) | `security-events` |
| `MAIN-SECURITY-0010` | runtime | Brute force: one account failed sign-in `failures` times within `window` | `security-events` |
| `MAIN-SECURITY-0011` | runtime | Credential stuffing: one source failed sign-in on `accounts` different accounts within `window` | `security-events` |
| `MAIN-SECURITY-0012` | runtime | Access probing: one actor was denied `denials` different resources within `window` | `security-events` |
| `MAIN-SIGNAL-0001` | runtime | `signals.emit` refused: the name isn't `app.*`, doesn't match the name pattern, the severity is unknown, or the data isn't JSON or is over 4KB | `signals` |
| `MAIN-SIGNAL-0002` | runtime | A signal's data attribute isn't in `telemetry@1` or matched a secret or personal-data detector; the attribute was dropped and named | `signals` |
| `MAIN-SIGNAL-0003` | runtime | A source passed `max-per-minute`; further emits were dropped (one record a minute with the count) | `signals` |
| `MAIN-SIGNAL-0004` | runtime | An app-scope or host-scope envelope failed verification (no signature where one is required, a bad signature, an unknown key id, a stale or future `at`); it was dropped and counted | `signals` |
| `MAIN-SIGNAL-0005` | runtime | A `signals.on` handler threw or took longer than 1s; the others still ran | `signals` |
| `MAIN-SIGNAL-0008` | runtime | A handler's queue was full; the oldest signals were dropped (one record a minute with the count) | `signals` |
| `MAIN-SYNC-0001` | runtime | A row was refused: the change names another tenant (the push fails, 403) or the person may not write the row (the row is reported and the rest goes through) | `sync` |
| `MAIN-SYNC-0002` | runtime | The device's schema version of a table differs from `sync.tables` in `main.yaml` (409); the device keeps its queue | `sync` |
| `MAIN-SYNC-0003` | runtime | The tenant is over `sync.quota` for rows, bytes or the size of one push (429); the push changes nothing | `sync` |
| `MAIN-SYNC-0004` | runtime | A change or call is not well formed, or names a table that is not in `sync.tables` (400) | `sync` |
| `MAIN-SYNC-0005` | runtime | A change carries a clock value further ahead of the server's clock than `sync.max-clock-drift`; that row is reported and dropped | `sync` |
| `MAIN-SYNC-0006` | runtime | The person was erased and can no longer sync (410); the device wipes its copy and queue | `sync` |
| `MAIN-TENANT-0008` | runtime | `tenant.create` or `auth.orgs.create` got input it can't use: no name, a name over 120 characters, a region other than `us`, `eu` or `in` (400), or an id or slug that is invalid or taken (409). Fix: pass `{ name, region }`, and an id such as `tnt_acme` or a slug nobody has | `tenancy` |
| `MAIN-TESTING-0001` | runtime | An interface in the catalog has no in-memory fake in the test kit, or its fake does not resolve | `testing-kit` |
| `MAIN-TESTING-0002` | runtime | A chaos drill names a target the kit does not know, or a helper that acts on the current test app runs before one exists | `testing-kit` |
| `MAIN-TESTING-0003` | runtime | A driver cannot be tested: it is not a `defineDriver` module, names another interface than it implements, or declares no manifest | `testing-kit` |
| `MAIN-TESTING-0004` | runtime | A conformance runner broke the line protocol: it printed a line that is not JSON, answered a case it was not asked, or exited without answers | `testing-kit` |
| `MAIN-TESTING-0005` | runtime | The testing kit is not installed in the project, or the `main.yaml` schema it exports vectors from is not built | `testing-kit` |
| `MAIN-TRUST-0001` | runtime | A Fleet contract cannot be activated because the trust page lacks required items (409). Fix: complete the missing items, then activate | `mainsdk-trust` |
| `MAIN-TRUST-0002` | runtime | A staff access to tenant data in the quarter did not use SSO, MFA and step-up (500). Fix: find how the access was made and fix the access path | `mainsdk-trust` |
| `MAIN-TUF-0002` | runtime | Metadata is malformed, or lacks the threshold of signatures its role needs | `tuf-delivery` |
| `MAIN-TUF-0003` | runtime | A target's bytes differ from the length or hash in the signed targets; the bytes are never handed out | `tuf-delivery` |
| `MAIN-TUF-0005` | runtime | A target is not listed in the current signed targets and snapshot (mixed release, unknown name, or not delivered yet) | `tuf-delivery` |
| `MAIN-TUF-0006` | runtime | A delegated role signed a path outside its delegation | `tuf-delivery` |
| `MAIN-TUF-0007` | runtime | Root cannot be trusted or updated: no installed root, or a new root not signed by both the old and the new keys | `tuf-delivery` |
| `MAIN-TUF-0008` | runtime | Refreshed: fresh metadata verified (logged at info) | `tuf-delivery` |
| `MAIN-TUF-0009` | runtime | The mirror is unreachable but the cached metadata has not expired (logged at info) | `tuf-delivery` |
| `MAIN-UPDATE-0001` | runtime | The client's version is below `updates.min-version`, or it named none under `strict` (426; the problem carries `min_version`) | `updates` |
| `MAIN-UPDATE-0002` | runtime | A version, platform or channel in a request or call isn't valid (400) | `updates` |
| `MAIN-VAULT-0001` | runtime | The vault has no location for the person's country, or its driver does not support it (422). Fix: add the country to `vault.locations` or pick a driver that supports it | `residency-vaults` |
| `MAIN-VAULT-0002` | runtime | The vault returned no token, alias or record for a write (502, retryable). Fix: retry, and check the vault credentials and location | `residency-vaults` |
| `MAIN-VAULT-0003` | runtime | A field marked `vault: true` has the label `pan` or a label a vault cannot hold (400). Fix: use `personal`, `sensitive`, `child` or `phi`; hand card data to `pay` | `residency-vaults` |
| `MAIN-VAULT-0004` | runtime | A blind index was asked of a vault driver that has none (400). Fix: look the person up by an id the app holds | `residency-vaults` |
| `MAIN-VAULT-0005` | runtime | The vault has not confirmed an erasure (502, retryable). Fix: wait for the erasure to retry; check the vault if it stays pending | `residency-vaults` |
| `MAIN-VERIFY-0001` | runtime | A code or link did not check out: unknown, expired, used, wrong, or out of tries. The call answers `ok: false` and the audit record carries this code; nothing says which | `verify` |
| `MAIN-VERIFY-0002` | runtime | Too many asks for one number, address or IP, or the bot-defence door or the pumping check said no (429); nothing was sent | `verify` |
| `MAIN-VERIFY-0003` | runtime | The provider could not deliver and said nothing about the recipient: an outage, a timeout or a rate limit (502, retryable) | `verify` |
| `MAIN-VERIFY-0005` | runtime | A number is not a valid E.164 number, or an address is not an email address (400) | `verify` |
| `MAIN-VERIFY-0007` | runtime | An identity call is malformed: unknown level, no market, person, purpose, claims or consent (400) | `verify` |
| `MAIN-VERIFY-0008` | runtime | An identity provider could not be reached or answered something unusable (502, retryable) | `verify` |
| `MAIN-VOICE-0006` | runtime | The agent couldn't join the room: the provider's API refused it, the signalling socket didn't confirm the room and identity, or the media bridge refused the token or didn't say it had joined in time (502, retryable) | `ai-voice` |
| `MAIN-WEB-0002` | runtime | Cross-site request refused: an unsafe method carried a cookie from another site, or no Fetch Metadata, no matching Origin and no valid token (403) | `inbound-protection` |
| `MAIN-WEB-0004` | runtime | A handler set a security header itself; the declared value replaced it (logged once per route and header; the fix is an `inbound.routes` entry) | `inbound-protection` |
| `MAIN-WEB-0005` | runtime | CORS preflight from an origin not in `cors.origins`, or with a method or header not allowed (403, no CORS headers) | `inbound-protection` |
| `MAIN-WEB-0006` | runtime | A CSP violation report arrived (warn; sampled to one record per directive and blocked URL per minute) | `inbound-protection` |
| `MAIN-WEB-0008` | runtime | A response set a cookie without `Secure`, `HttpOnly` or `SameSite`; the adapter added them (logged once per cookie name) | `inbound-protection` |
| `MAIN-WEB-0012` | runtime | `security.txt` expires within 30 days (warn, once at start and once a day) | `inbound-protection` |
| `MAIN-WEB-0013` | runtime | `inbound.nonce()` or `inbound.csrfToken()` was called outside a request (500) | `inbound-protection` |
| `MAIN-WORKER-0002` | runtime | A worker module that `workers.modules` or a pool's `modules` doesn't declare (403 in enforce mode) | `parallelism` |
| `MAIN-WORKER-0003` | runtime | A task's input or result can't be cloned (a function, a symbol, a class with private state); names the path; nothing was sent (400) | `parallelism` |
| `MAIN-WORKER-0004` | runtime | A task's input holds a resolved secret; nothing was sent, and the value is never echoed (403) | `parallelism` |
| `MAIN-WORKER-0005` | runtime | A task passed its timeout or the context's deadline; its thread or process was terminated and replaced (504) | `parallelism` |
| `MAIN-WORKER-0006` | runtime | A task's thread or process ran out of memory, exited or crashed; it was replaced (500) | `parallelism` |
| `MAIN-WORKER-0007` | runtime | The pool's queue is full (503, `retryable`, `retryAfter` 1) | `parallelism` |
| `MAIN-WORKER-0008` | runtime | A call inside a worker module to an interface that isn't available there (secrets, network, sockets, disk, db, jobs and the rest); names the call (500) | `parallelism` |
| `MAIN-WORKER-0011` | runtime | A cluster instance exited without being asked; it is respawned after the backoff (logged with slot, exit code and signal) | `parallelism` |
| `MAIN-WORKER-0012` | runtime | A cluster slot crashed `crashes` times within `window`; the primary stopped respawning it, and exits 1 when no instance is left | `parallelism` |
| `MAIN-WORKER-0013` | runtime | A rolling restart stopped: a new instance wasn't ready within `start-timeout`; the old instances keep serving | `parallelism` |
| `MAIN-WORKER-0014` | runtime | Parallelism isn't available here, so tasks run inline or the cluster runs one process; logged once with the reason (runtime, `mainsdk dev`, serverless) | `parallelism` |
| `MAIN-WORKER-0015` | runtime | Processes times heap limit, plus worker threads times `memory`, is above the container's memory limit; names the `--max-old-space-size` to set per process (logged once at start) | `parallelism` |
| `MAIN-WORKFLOW-0013` | runtime | `workflow()` got a name that isn't lowercase letters, digits, dots, dashes and underscores, or a version that isn't a positive whole number (400). Fix: write a valid name and `version: 1`, then 2 when the steps change | `workflows` |
<!-- codes:end -->

## Debug an error

```text
$ npx mainsdk explain 4bf92f3577b34da6a3ce929d0e0e4736
MAIN-DATA-0007  Record belongs to another user
at     app/api/orders/[id]/route.ts:18 (getOrder)
rule   data.owner_scope
fix    Scope the query with db.orders.for(user).
```

`mainsdk explain` works offline from the local registry and event store. To see each provider call (driver, operation, duration, status, with bodies and headers redacted), set `MAINSDK_DEBUG=driver`. In production it is refused with `MAIN-LOG-0003` unless a debug session allows it; see [environment variables](https://mainsdk.dev/docs/reference/environment).

## Errors from MainSDK Cloud

> **Note:** Phase 2. Cloud errors use the same shape.

When the Free allowance runs out after the warning and grace period, MainSDK Cloud answers 402. Only the Cloud: your app keeps running and every check keeps working.

```json title="402 application/problem+json"
{ "type": "https://mainsdk.dev/errors/MAIN-CLOUD-0402", "title": "Free allowance used",
  "status": 402, "detail": "50,000 events used this month. Add a card or move to Teams.",
  "upgrade": "https://mainsdk.dev/billing?prj=prj_01JC8Q4W7ZK3M5N8P2R6T9V0XY" }
```

## Next

- [Check codes](https://mainsdk.dev/docs/reference/check-codes)
- [Debugging](https://mainsdk.dev/docs/guides/debugging)
- [Environment variables](https://mainsdk.dev/docs/reference/environment)
