---
title: Error codes
order: '22'
summary: "Every SCWP / SCSG / scratch / public-share error code \u2014 meaning, wire\
  \ name, and detail shape."
path: reference/errors
status: published
---

Every error frame carries `{1: code, 2: wire_name, 3: message, 4: detail}`. Clients switch on `code` for machine-handling; log `message` for humans; read `detail` for context-specific data.

## Core SCWP errors (SCWP §8)

| Code | Wire name | Meaning | `detail` shape |
|---|---|---|---|
| `1` | `E_MALFORMED` | Frame or body couldn't be parsed | `{}` |
| `2` | `E_UNAUTHORIZED` | Missing / invalid auth | `{}` |
| `3` | `E_NOT_FOUND` | Channel / envelope / device / wrap missing | `{}` |
| `4` | `E_FORBIDDEN` | Caller not permitted (member check, tenant policy, etc.) | Varies |
| `5` | `E_ALREADY_CLAIMED` | One-shot clip has been claimed | `{claimed_by: text, at: u64}` |
| `6` | `E_GONE` | Envelope tombstoned (expired / claimed / deleted / revoked) | `{}` |
| `7` | `E_RATE_LIMITED` | Rate limit exceeded | `{retry_after_ms: u64}` |
| `8` | `E_TOO_LARGE` | Frame or ciphertext over ceiling | `{limit: u64, actual: u64}` |
| `9` | `E_KEY_STALE` | `key_version` older than the channel's current | `{current: u32}` |
| `10` | `E_EXISTS` | Team channel name collision | `{}` |
| `11` | `E_WRAPS_INCOMPLETE` | `ONE_SHOT` publish missing wraps for member devices | `{missing: [device_id]}` |
| `12` | `E_BAD_SIG` | Envelope signature failed verification | `{}` |
| `13` | `E_NOT_ONESHOT` | `clip.claim` on a non-`ONE_SHOT` clip | `{}` |
| `14` | `E_QUOTA` | Legacy quota exceeded (deprecated) | `{}` |

## SCSG / scratch / media errors (v0.1.3+)

| Code | Wire name | Meaning | `detail` shape |
|---|---|---|---|
| `15` | `E_QUOTA_EXCEEDED` | Scratch quota or SCSG concurrent-session cap | `{scope, used, limit}` |
| `16` | `E_UNKNOWN_SERVICE` | `session.open` with unregistered `service_id` | `{service_id: text}` |
| `17` | `E_NO_ONLINE_DEVICE` | SCSG open can't find an online T1 device for the user | `{user_id: text}` |
| `18` | `E_OUT_OF_SCOPE` | SCSG operation outside grant's `channel_ids` or missing a required capability | `{granted, requested}` |
| `19` | `E_GRANT_EXPIRED` | SCSG grant is closed / denied / expired / revoked / past TTL | `{status: text}` |

## Media proxy errors (planned)

| Code | Wire name | Meaning |
|---|---|---|
| — | `E_ORIGIN_OFFLINE` | `media.fetch` for `ref_origin` with origin device offline (returned as generic error today; distinct code lands in v0.1.4) |
| — | `E_TRANSFER_TIMEOUT` | `ref_origin` transfer exceeded `scaiclip_mediaproxy_transfer_timeout_s` |
| — | `E_HASH_MISMATCH` | Scratch commit sha256 doesn't match reservation |
| — | `E_TOO_MANY_TRANSFERS` | Per-channel concurrent-transfer cap hit |

## HTTP errors

Public-download route (`GET /p/{token}`) uses standard HTTP codes:

| Status | Meaning |
|---|---|
| `200` | Landing page returned (password-bound link) |
| `302` | Redirect to presigned S3 URL |
| `404` | Non-existent token (constant-time; indistinguishable from `410`) |
| `410` | Token revoked / expired / download-limit reached / underlying object gone |
| `429` | Per-IP or per-token rate limit exceeded |

REST endpoints on `/v1/modules/scaiclip/…` map service errors to standard 400/403/404 with the wire-name / message in the JSON envelope.

## Retry semantics

Errors safe to retry as-is:

- `E_RATE_LIMITED` — wait `retry_after_ms`, retry.
- `E_NOT_FOUND` on `key.wraps.get` — wait for a `wraps.ready` push, retry.
- `E_TRANSFER_TIMEOUT` — restart `media.fetch`.

Errors that require caller action first:

- `E_KEY_STALE` — re-run `channel.list` + `key.wraps.get`.
- `E_WRAPS_INCOMPLETE` — call `channel.devices`, generate wraps for missing devices, retry `clip.publish`.
- `E_ORIGIN_OFFLINE` — surface to user ("sender is offline").
- `E_QUOTA_EXCEEDED` — surface with the `oldest_reclaimable` hint from `detail`.

Errors that should not be retried without a fix:

- `E_MALFORMED`, `E_BAD_SIG`, `E_UNAUTHORIZED`, `E_FORBIDDEN` — these are protocol / auth bugs on the caller side.

## Audit correlation

Every rejection emits an audit event with the same `resource_type` / `resource_id` as the successful path. Filter for `status = 'DENIED'` in `/v1/audit` queries to find where callers hit these errors.

## Related

- [SCWP wire reference](./scwp.md)
- [SCSG verb reference](./scsg.md)
- [Troubleshooting](../troubleshooting.md) — common causes and fixes for each error.