Appearance
Errors
Every error is an RFC 9457 problem document with a stable, namespaced code. Match on the code, not the HTTP status or the prose. The code is the contract and never changes within v1.
json
{
"type": "https://opaline.dev/problems/pack.lint_failed",
"title": "pack.lint_failed",
"status": 422,
"code": "pack.lint_failed",
"detail": "pack.hard_without_citation"
}The Content-Type is application/problem+json. The SDK surfaces this as a typed error with code, title, and detail.
Idempotency
| Code | Status | When |
|---|---|---|
idempotency.key_required | 400 | decide was called without an Idempotency-Key header. |
idempotency.payload_mismatch | 422 | The same key was reused with a different body. Use a new key, or resend the original body. |
idempotency.in_flight | 409 | The same key is still being processed. Retry after Retry-After. |
Payload
All four guard the determinism boundary: send numbers as decimal strings.
| Code | Status | When |
|---|---|---|
payload.float_rejected | 400 | A JSON float appeared in a hash-input position. Send it as a string ("0.1"). |
payload.integer_out_of_range | 400 | An integer outside ±(2⁵³−1). Send it as a string. |
payload.duplicate_key | 400 | A JSON object had a duplicate key. |
payload.malformed_json | 400 | The body is not valid JSON. |
payload.too_large | 413 | The request exceeds the size cap. |
Auth
| Code | Status | When |
|---|---|---|
auth.invalid_key | 401 | The key is missing, malformed, revoked, or wrong for the plane. |
auth.scope_missing | 403 | The key is valid but lacks the scope the operation needs. |
Packs
| Code | Status | When |
|---|---|---|
pack.not_found | 404 | Unknown pack id, version, or alias. |
pack.invalid | 400 | The pack document is malformed or structurally invalid. |
pack.lint_failed | 422 | A lint rule failed (e.g. a hard rule without a citation). detail names the rule. |
pack.embedded_test_failed | 422 | An embedded test vector failed during the publish gate. |
pack.version_immutable | 409 | Re-publishing an existing id@version with different content. |
Rate & quota
| Code | Status | When |
|---|---|---|
rate.limited | 429 | The per-tenant rate limit was exceeded. Back off per Retry-After. |
quota.exceeded | 429 | The tenant's monthly decide quota is exhausted. |
Request & service
| Code | Status | When |
|---|---|---|
request.rejected | 400/413 | The request was rejected by a structural or size guard. |
tenant.not_found | 404 | Unknown tenant (internal provisioning surface). |
simulate.snapshots_unavailable | 422 | Trail-history simulate needs a snapshot-retention tenant. |
service.unavailable | 503 | Fail-closed: the engine refused to decide without a durable trail. Safe to retry. |
Retrying
- Safe to retry (transient):
idempotency.in_flight,rate.limited,service.unavailable. HonourRetry-Afterwhere present. - Do not retry unchanged (deterministic): everything else. Fix the request, the key, the scope, or the pack first.
Because decide is idempotent, retrying a timed-out request with the sameIdempotency-Key and body is always safe: it replays the stored response rather than deciding twice.