Troubleshooting
Fast-path fixes for the errors we see most often. Skim by symptom.
Client can't connect to the WebSocket#
Symptom. Your client library returns an HTTP 401 / 403 during upgrade, or the connection closes immediately with code 4401 / 4403.
- 401 or
4401— auth failed. Bearer token missing, expired, or malformed. Also fires when you passed ansgk_API key but the tenant has revoked it. 4403— auth resolved but the tenant context is missing (rare; means the caller has no tenant assigned) or the SCSG endpoint got a non-service principal.- Double-check
?token=…vsAuthorization: Bearer …. Both work; only pass one. SCAIGRID_AUTH_DISABLED=trueon the server disables all auth. If you're running local dev and can't connect, that's likely the missing flag.
device_id query param required#
Symptom. WS close code 4403 with that reason on the SCWP endpoint.
The ?device_id=… query parameter is required on every scaiclip/1 connection. It must be either a 32-char hex string (16 raw bytes) OR a ScaiKey nanoid (dev_XYZ…).
E_BAD_SIG on clip.publish#
- Signature was made over the wrong subset of envelope keys. Signature input is canonical CBOR of keys 1-14 only. Keys 3 (seq) and 7 (grid_ts) are grid-assigned; do not include them.
- Sender
sig_pubdoesn't match what ScaiKey has on file. Common cause: you rotated device keys client-side but didn't callPOST /me/devices/{id}/keyson ScaiKey. - Endian mismatch in the CBOR encoder. Canonical CBOR means definite-length maps sorted by canonical CBOR key ordering. If your library reorders map keys, signatures will fail.
E_WRAPS_INCOMPLETE on ONE_SHOT publish#
Symptom. You published a ONE_SHOT clip and got E_WRAPS_INCOMPLETE { missing: [dev_xyz, …] }.
The grid requires one-shot wraps for every current member device at publish time. If a new device onboarded between when your client fetched channel.devices and when you published, that device is missing a wrap.
Fix: re-query channel.devices, generate a fresh wrap for each missing device, retry clip.publish.
E_ALREADY_CLAIMED#
Someone else claimed the one-shot before you. detail.claimed_by names the winning subject; detail.at is the ms timestamp.
Not an error condition — this is how one-shot works. Your UI should show the user "already claimed by X" rather than retry.
E_RATE_LIMITED#
You're publishing / reserving / opening sessions faster than the token bucket allows. detail.retry_after_ms says when to try again.
Default rates:
clip.publish— 30/s per device with 60-token burst.scratch.reserve— 2/s per user with 10-token burst.- SCSG
session.open— 5/s per(tenant, service_id)with 20-token burst. - Public link mint — 30/min per tenant with 60-burst.
If you're bumping the limit on legitimate load, ask for an uplift.
E_QUOTA_EXCEEDED on scratch#
Symptom. scratch.reserve fails with detail = {scope: "user" | "tenant", used, limit, additional}.
- Delete old scratch objects (
scratch.delete) —listfirst to see what you have, and check theoldest_reclaimablehint in the error detail. - Ask the tenant admin to raise the quota (setting:
scaiclip_scratch_quota_user_bytes/_tenant_bytes). - Consider
ref_originmode for the specific clip — bypasses scratch storage entirely.
E_ORIGIN_OFFLINE#
You tried to media.fetch a ref_origin clip while the origin device isn't connected. There's no queuing — ref_origin is deliberately ephemeral.
Surface a UX like "sender is offline; ask them to reopen ScaiClip". If the content matters over the origin's presence, the sender should have published with ref_scratch instead.
E_FORBIDDEN — SCSG session refuses to open#
Possible causes, in order of frequency:
- Tenant hasn't opted your service in. Check
mod_scaiclip_tenant_policy.allow_scsg_<service_id>. - You requested
CLAIMcapability but tenant hasn't opted in for one-shot consumption. That gate is onallow_agent_claim. - Your
client_iddoesn't map to a registeredservice_idper the SCSG registry. Prefix yourclient_idwithcli_<service_id>_.
Error detail includes service_id and tenant_id for triage.
E_NO_ONLINE_DEVICE#
You're trying to open an SCSG session but the user has no device connected. Grid needs at least one online T1 to consent-prompt.
Client-side UX: prompt the user to open their ScaiClip client first, then retry.
Public download link returns 410#
The link is dead. Possibilities:
- Expired (
expires_atin the past). - Revoked (owner or via
revoke_token). - Download limit reached.
- Underlying scratch object was reclaimed (rare — happens if scratch idle-TTL fires before public expiry).
The 410 is returned in constant time; the specific reason isn't disclosed to the anonymous fetcher. Owners can look up the token record via the API to see what happened.
Retention engine isn't sweeping#
Symptom. Sensitive clips outlive their TTL by many seconds, or expired scratch objects stick around.
- Confirm the retention engine started. Log line
scaiclip_retention_startedshould appear in module init. - Check
is_running— all four loops (sensitive, normal, scratch, SCSG) must be alive. A crash in one loop stops that specific sweep but not the others. - If the process just restarted, the first sweep runs
_sensitive_cadence_sseconds after start (default 15 s), not immediately.
Presence heartbeats aren't reaching ScaiKey#
- Confirm
PresenceHeartbeatPusherstarted (log linescaiclip_presence_started). - Check the shared ScaiKey client is configured —
SCAICLIP_SCAIKEY_URLorSCAIKEY_BASE_URLmust be set. - Test the heartbeat batch endpoint directly with a service token. If the platform returns 401/403, the ScaiKey OAuth scopes on the ScaiGrid application don't include
devices:heartbeat.
Audit rows aren't landing#
- Audit is best-effort — it never breaks the calling path. Failures log as
scaiclip_audit_emit_failed. - Common cause:
audit_logtable missing. Runalembic upgrade headon the ScaiGrid schema, not just the module schema. - Filter with
action LIKE 'scaiclip.%'in/v1/auditqueries.
Where to look next#
- Every action lands in
audit_logwith matchingresource_type/resource_id. Filter bystatus = 'DENIED'for rejections. - Structured log lines with prefix
scaiclip_cover retention sweeps, presence pushes, webhook receipts, and SCSG lifecycle. Grep the ScaiGrid pod logs. - Metrics on
RetentionEngine.expired_sensitive_total/_normal_total/reclaimed_scratch_total/expired_scsg_totalcounters expose sweep health. - The error codes page maps each
E_*to what it means and what to do.
If none of the above matches your symptom, open an issue with:
- Wire dump of the failing frame (both directions).
- Server-side
scaiclip_log lines around the failure. - Approximate wall-clock at time of failure so we can correlate with audit rows.