Failure semantics
Nothing in Belora stalls silently. Every failure has a code and every wait has a limit; this page lists the ones a client can observe.
Errors and what to do
There are 19 error codes, all in one envelope; the errors reference lists which operations can answer each. Match on code, never on message. A mutating call is safe to retry with the same Idempotency-Key for 1 day; a retry while the first is still running answers 409 until the first finishes, or 1 minute pass and it is presumed crashed.
| Code | Status | The client |
|---|---|---|
| unauthorized | 401 | Stop. The key is missing, malformed, unknown, revoked or expired, or its person is gone. Mint a new key. |
| forbidden | 403 | Stop. A conformance key called an operation outside its fence, or a production key called a conformance-only one. Do not retry. |
| not_found | 404 | Stop. The route or the id does not exist in your org. An id from another org looks the same. |
| validation_failed | 400 | Fix the request. Each entry of issues names a path under params, query or body. Do not retry unchanged. |
| conflict | 409 | Read details.reason or the message. Retry only a conflict that says it is in flight, with the same Idempotency-Key. |
| rate_limited | 429 | Wait for the Retry-After header, then retry the same request. |
| internal | 500 | Retry with the same Idempotency-Key. If it persists, report the request. |
| person_not_found | 404 | The person named when minting a key does not exist or is inactive. Choose an active person. |
| person_not_verified | 403 | The person has no verified login link. Verify it, then mint again. |
| person_mismatch | 403 | A person key can be minted only by the person it is for. |
| key_not_found | 404 | The key id does not exist in your org. |
| key_lacks_capability | 403 | Use a key whose tier and role hold the capability named in details. A service key can never hold a judgment capability. |
| key_cannot_commit_judgment | 403 | Use a person key. A service key never commits a verdict, proposal or claim. |
| answerer_not_a_person | 403 | Only a person can answer. Use a person key. |
| answerer_unlinked | 403 | The answerer has no Belora identity in this org. Link the identity. |
| answerer_not_verified | 403 | The answerer's identity is not verified. Verify it, then answer. |
| answerer_lacks_capability | 403 | The answerer's role lacks the capability the decision needs; details names it. Ask someone who holds it. |
| not_a_judge | 403 | The person holds no judge seat, so their key cannot commit a verdict. Ask an owner for a judge seat or use a judge's key. |
| decision_not_found | 404 | No such decision in your org. |
Proved by errors.envelope
Expiries and escalation
Waiting states are swept by one registry that ticks every 6 hours, so most steps land within a tick of their threshold; a lease and a quick bet also have a timer of their own. Most decays are recorded as a decay event; the few that only bound bookkeeping are not. An unanswered decision is nudged, then re-routed, and is never silently dropped.
| Policy | After | What happens |
|---|---|---|
| hypotheses.expire | 30 days after its newest evidence | An open or corroborated hypothesis expires and leaves the open list. New evidence reopens it. |
| proposals.stale | its theme shipped or was archived | A pending proposal on that theme is marked stale. |
| decisions.nudge | 2 days after the decision is raised | Its owner is nudged once. decision.escalated, step nudge. |
| decisions.reroute | 2 days after the nudge | The org owner is brought in once. The owner keeps it, so either may answer. decision.escalated, step reroute. |
| tasks.pauseEscalate | 2 days, then 2 days | A job paused on a question escalates on the same clocks. |
| tasks.quickDecisionExpire | the quick bet's window, 1 minute to 4 hours | The bet settles as it was declared: the job pauses for a person (block) or reopens on the stated assumption (proceed). |
| tasks.leaseExpire | 2 hours after the claim | The job returns to open. When its attempts have reached 10 it fails instead; a release does not count. job.expired, reason lease. |
| tasks.closingExpire | 1 day in closing | A closing job whose pull request has not merged fails. job.expired, reason closing. |
| proposals.review | 3 days without a verdict | The decider is reminded, once per quiet period. |
| sessions.expire | 30 minutes without a message | The session expires. Its messages and context are pruned 7 days after it ends; the session and its commit stay. |
| webhookDeliveries.attemptLost | 5 minutes with an attempt that never reported | The delivery fails with lastError attempt lost, and replay accepts it. |
Events and webhook deliveries are kept for 90 days; a delivery whose event passed that fails.
Stated, not run by the suite: decision.escalation hypothesis.expire jobs.expiry
Webhook failure
A delivery is pending, delivered, failed and gets at most 6 attempts. An endpoint is disabled with reason key_revoked, key_lapsed, deleted. The schedule, the reasons and replay are in the webhooks guide.
Stated, not run by the suite: webhooks.retry webhooks.key_lapsed
Proved by
Every claim on this page is one of these checks; those marked stated are documented behaviour the suite cannot drive. Run them against your org:
npx @belora/conformance --key $BELORA_CONFORMANCE_KEY --api $API --mcp https://mcp.belora.dev/mcp
- errors.envelope Every failure is the registry envelope
Each error the registry declares for an operation answers its published status in the envelope { error: { code, message } } and nothing else.
- decision.escalation An unanswered decision escalatesstated, not run by the suite
An open decision is nudged to its owner once after the published nudge window and re-routed to the org owner once after the published re-route window, each emitting decision.escalated.
- hypothesis.expire An unsupported hypothesis expiresstated, not run by the suite
An open or corroborated hypothesis with no new evidence for the published window becomes expired and leaves the open list; new evidence reopens it.
- jobs.expiry A lapsed lease requeues the jobstated, not run by the suite
A job whose lease passes its term without a close or release returns to open with attempts kept and a job.expired event with outcome requeued, and fails with outcome failed once the published reclaim cap is reached.
- webhooks.retry A failed delivery retries on the published schedulestated, not run by the suite
An endpoint that answers non-2xx sees the same Belora-Delivery id again after each published backoff, a fresh signature each time, and the delivery ends failed with its attempt count after the last try.
- webhooks.key_lapsed An endpoint stops with its keystated, not run by the suite
Revoking the creating key disables its endpoints with disabledReason key_revoked; a key that expires or loses webhook:manage disables them with key_lapsed at the next event; a live key brings them back with POST /v1/webhooks/{id}/enable.