Skip to main content
Sanning’s errors fall into three groups, and telling them apart saves the most time.
  • Not yet. The plane is settling. Wait and retry.
  • Wrong shape. Your call or your key is wrong. Change something and retry.
  • Refused. Sanning is declining on purpose. Read the reason.

Not yet

These resolve on their own. Retry rather than debugging.
The interval containing your records has not been sealed and published yet. It means not yet, never you have no evidence. If records in the window are anchored and waiting for a stamp, a second line names how many.Wait for the interval to close and run the command again. See The witness.
Some records in the period are stamped and some are not, so a pack built now would leave the unstamped ones out. Nothing inside a pack records what it omitted, so the recipient could not tell it was short for the period it names. The verb refuses instead.Wait for the interval covering them to seal and run the command again. To hand over the stamped records anyway, pass --allow-unstamped, and say in the cover note how many were left out. See The witness.
A record anchored seconds ago is not readable through the read routes yet. Placement settles after the write returns.Wait and retry. Building a pack immediately after a run races your own newest record.

Wrong shape

Two different refusals produce this line, and the response body tells them apart.If it carries a sentence naming an address to write to, this is not a key you built wrong: every key you create already carries anchor:read, so the refusal means your organization is not enabled for that route. Your records are intact and anchored; the credential cannot read them back.If it carries no sentence, only required and the key’s own keyScopes, the key itself genuinely lacks the scope. This is the case for a key an operator minted through POST /api/v1/admin/keys, which is not bound to the two-scope-set rule POST /api/v1/user/keys enforces. Check what the key was minted with.See API keys and scopes.
--logs is required in practice. The command reads your record objects to work out which events belong to this agent, because a minimum-disclosure envelope does not carry that name.Point it at your store root, the directory holding content/ and records/.
--logs accepts an s3:// URL, and reading one needs SANNING_S3_ENDPOINT, SANNING_S3_ACCESS_KEY_ID and SANNING_S3_SECRET_ACCESS_KEY in the environment. The message names the ones that are missing.There is no flag for them: a credential in argv lands in shell history. See Keep what you anchored.
The pack covers a period and you must name it. --agent, --from, --to and --out are all required.A date-only bound names a whole day. Pass a full timestamp such as 2026-09-01T09:00:00Z for a narrower window.
The window you asked for includes records whose bytes are not in the store you pointed at. Usually the window is wider than you meant.The command refuses rather than quietly dropping them, because a pack that silently omits records an auditor asked for is worse than no pack.
The plane’s records route filters by --agent on the server, so a name that does not match an agent on your roster stops there before bundle reads anything back. The command prints bundle: no stamped records for agent "…" with this reason, and exits 1.Check the spelling against the console’s Agents page. --agent takes the name you anchored under, not a display name you renamed it to later.
From 0.15.0. bundle --case with SANNING_SIGNING_SEED set found records in the window that name no case, so it cannot tell whether they belong to the case it is declaring.Pass the case id on every record in the case when you anchor it (caseId in TypeScript, case_id in Python), then run bundle again. See Name the case on every record.
From 0.15.0. bundle without --case, with SANNING_SIGNING_SEED set, found records that name a case. A declared pack names one case, so it refuses rather than put those records under the agent’s name.Pass --case and run the command once per case.
The pack has no logs/ directory. A pack of hand-off records has none, because no hand-off record discloses content. Run verify without --logs.
You handed the kernel a zip. PK is the archive’s own signature.Extract it and point at bundle.json inside. See Verify at the command line.

Refused

The plane accepts records only from keys the organization knows, so that every record has an identity behind it.Both SDKs enroll automatically before the first anchor. If you see this, the signing key reaching the plane is not the one that enrolled. The message names the two calls that enroll a key by hand, if you drive enrollment yourself.
That agent name was revoked. Revoking is deliberate and a revoked name cannot be re-enrolled by anyone, including whoever holds the old key.Recovering keeps the lineage: an owner or admin pins a new key for the name from Settings, Signing keys in the console, then the agent completes the re-key by restarting with that key. See Re-key a revoked name.
Your API key is bound to a different agent_id than the one in the request, and a key bound to one agent 403s for every other one.A key you create in the console is never bound to an agent, so this appears only on a key Sanning bound for you. Use an ordinary key from Settings, API keys, which covers your whole fleet.
The proof-of-possession challenge is missing, expired, already used, or was not issued for this agent. Request a fresh challenge and sign it straight away.
Proof-of-possession failed: the signature does not verify against the public key the challenge names. Check that the signer holds the key you believe it holds.
This agent already has an active signing key, and rotating it needs previous_signature, the same challenge signed by the outgoing key. If that key is lost, an owner or admin recovers the agent by pinning a new key from Settings, Signing keys, which the agent then claims by restarting with it.
The signature verifies, but this agent has no active signing key left to rotate: its only live key is still draining from an earlier rotation. The SDK will not rotate off a draining key, so an owner or admin pins a new key for the agent from Settings, Signing keys instead, and the agent claims it on its next restart.
This public key is already enrolled somewhere in Sanning, under this name or another, in your organization or a different one, whether active, retired or revoked. A signing key is unique across every organization, not only your own, and cannot be reused for a different agent or as a rotation back onto an earlier key. A key revoked in your own organization answers with the more specific SIGNING_KEY_PREVIOUSLY_REVOKED instead. The message names no other organization. Generate a new key where the agent runs and enroll that one instead. Nothing was changed.
This public key was revoked in your organization, so it cannot be enrolled again under any name there. Revoking a key repudiates it at every time, whichever name held it. A key revoked in a different organization is refused too, as SIGNING_KEY_ALREADY_ENROLLED, and so is a key belonging to an organization deleted since. Generate a new key where the agent runs and enroll that one instead. Nothing was changed.
A console re-key was asked for a name that is not revoked: one of its keys is still active, or the name has already been re-keyed. Rotate a live key through the SDK instead, which asks the outgoing key to counter-sign. Nothing was changed.
The SDK writes your bytes to your log store before anchoring, and a store failure stops the anchor. There is no best-effort mode on this path.Anchoring a commitment to bytes you cannot produce is the one state that cannot be repaired afterwards. See Keep what you anchored.

Verdicts are not errors

partial and undetermined are answers, not failures. A check that could not run is reported as not run. See What a verdict means.