# The four decisions

`POST /api/v1/authorize` always answers with exactly one `decision`. Handle it the same way every time.

| `decision` | meaning | binding `status` | what your code does |
|---|---|---|---|
| `AUTHORIZED` | within effective authority | `AUTHORIZED` (dry run: `DRY_RUN`) | act - in `binding` mode you hold a single-use authorization until `expires_at` |
| `APPROVAL_REQUIRED` | a human owner must approve | `APPROVAL_PENDING` | wait: poll or subscribe; never retry to force it -> [APPROVAL_REQUIRED.md](APPROVAL_REQUIRED.md) |
| `DENIED` | outside authority / policy / task / envelope | `DENIED` | stop; surface `denial_reasons[]`; do **not** retry with altered wording -> [DENIED_AND_REASON_CODES.md](DENIED_AND_REASON_CODES.md) |
| `NEEDS_INFORMATION` | a material term or an authority is missing | `NEEDS_INFORMATION` | declare the missing term, or relay the **one** `question` to the human -> [NEEDS_INFORMATION.md](NEEDS_INFORMATION.md) |

## The core response (read these fields first)

A response carries ~60 top-level fields; most are evidence. These are the ones a client needs:

| field | type | when | meaning |
|---|---|---|---|
| `decision` | enum | always | one of the four |
| `mode` / `status` | enum | always | `dry_run` -> `DRY_RUN`; `binding` -> `AUTHORIZED` / `APPROVAL_PENDING` / `DENIED` / `NEEDS_INFORMATION`, later `CONSUMED` / `EXPIRED` / `REVOKED` / `APPROVAL_REJECTED` |
| `authorization_id` | `auth_...` | always | the record id (dry runs get one too, for `/explain`) |
| `effective_maximum_minor` | int | always | the money ceiling after intersecting every layer |
| `autonomous_maximum_minor` | int | always | above this (and within the effective max) a human must approve. It **reports** the owner's ask-first line (`policy.transaction_auto_limit`, narrowed by any tighter layer): `2000` under the Golden Path authority; `0` when the owner set the line to `$0.00` or `autonomy_level: 1` (every consequential action asks first); equal to `effective_maximum_minor` when no ask-first line is set. `GET /authority/me` / MCP `authority_discover` return the same value the engine uses - discovery never widens it |
| `effective_authority.binding_layer` | enum | always | which layer set the ceiling: `organization`, `agent_standing`, `task`, `envelope`, `delegation`, `ttl` |
| `denial_reasons[]` | reason[] | DENIED | why; each has `code`, `message`, `family`, `severity`, `remedy` |
| `approval_reasons[]`, `approval` | reason[], object | APPROVAL_REQUIRED | `approval.approval_id` (binding only; dry run shows `state: would_be_required`) |
| `needs_information[]` | reason[] | NEEDS_INFORMATION | the missing terms (`field`, `code`) |
| `missing_authority[]` | gap[] | NEEDS_INFORMATION (envelope) | `dimension`, `needed`, `have`, `how_to_obtain` |
| `question`, `question_required` | object, bool | APPROVAL_REQUIRED / NEEDS_INFORMATION | the ONE latest safe question for the human (may be `null` when only a term is missing) |
| `expires_at` | ISO 8601 | binding | authorization TTL (default 15 minutes; `ttl_seconds` 30 s - 24 h; the earliest of TTL / task / envelope / delegation expiry wins) |
| `execution_plan` | object | always | `execution_mode` `external` / `quattrino`, `adapter`, `how`, `quattrino_moves_money` |
| `portable_proof` | object | binding + AUTHORIZED + external | `token` (EdDSA JWS), `proof_id`, `audience` -> [PROOF_VERIFICATION.md](PROOF_VERIFICATION.md) |
| `compliance` | object | after `/outcome` | `COMPLIANT` / `NONCOMPLIANT` + `findings[]` |

Everything else (`layers`, `dimensions`, `evidence_hash`, `material_terms_hash`, `aggregate_accounting`,
`advisories`, `pipeline`...) is evidence and is explained by `GET /authorizations/{id}/explain`.

## One handler, four branches (Python, SDK)

```python
from quattrino import Quattrino, AUTHORIZED, APPROVAL_REQUIRED, DENIED, NEEDS_INFORMATION
q = Quattrino(api_key="qtrn_sk_key_...", base_url="https://quattrino.io")

def handle(r):
    if r.decision == AUTHORIZED:
        return "go", r.authorization_id
    if r.decision == APPROVAL_REQUIRED:
        return "wait", (r.get("approval") or {}).get("approval_id")      # binding only; see APPROVAL_REQUIRED.md
    if r.decision == DENIED:
        return "stop", [d["code"] for d in r.get("denial_reasons") or []]
    if r.decision == NEEDS_INFORMATION:
        if r.question:                                                   # authority missing -> ask the human ONE question
            return "ask_human", r.question["question"]
        return "declare", [n["field"] for n in r.get("needs_information") or []]   # term missing -> supply it
```

The Golden Path amounts (standing $50, ask-first above $20):

```python
handle(q.authorize(amount_minor=1999, merchant="Amazon", purpose="USB-C cable", commitment="ONE_TIME", task={"max_amount_minor": 2000}))
# ('go', 'auth_...')
handle(q.authorize(amount_minor=2500, merchant="Amazon", purpose="USB-C hub", commitment="ONE_TIME"))
# ('wait', None)  -- dry run; a binding call returns approval_id
handle(q.authorize(amount_minor=7500, merchant="Amazon", purpose="monitor", commitment="ONE_TIME"))
# ('stop', ['exceeds_effective_authority', 'transaction_limit_exceeded'])
handle(q.authorize(merchant="Amazon", purpose="cable", commitment="ONE_TIME"))
# ('declare', ['amount_minor'])
```

## Dry run vs binding

| | `mode: dry_run` (default) | `mode: binding` |
|---|---|---|
| issues an authorization | no (`status: DRY_RUN`) | yes (`status: AUTHORIZED`, single use, `expires_at`) |
| reserves standing authority | no | yes for `execution: external` (released on outcome / revoke / expiry) |
| creates an approval | no (`approval.state: would_be_required`) | yes (`approval.approval_id`, owner inbox, webhook) |
| portable proof | never | yes when `AUTHORIZED` + `execution: external` |
| `Idempotency-Key` header | optional | **required** (`422 idempotency_key_required` otherwise) |

## Reason object (shared by `denial_reasons`, `approval_reasons`, `needs_information`)

```json
{ "code": "exceeds_effective_authority", "canonical": "exceeds_effective_authority",
  "message": "The amount exceeds the effective authority (agent_standing).",
  "family": "AMOUNT_LIMIT", "severity": "deny",
  "remedy": "Lower the amount, or ask the owner to raise the task / envelope limit (owner_amendment).",
  "effective_maximum_minor": 5000, "requested_minor": 7500, "binding_layer": "agent_standing",
  "registry_version": "1.0.0" }
```

`severity` is one of `deny`, `approval`, `information`, `invalid`, `advisory`. The registry is public:
`GET /api/v1/public/reason-codes` (246 codes, 24 families) and `GET /api/v1/public/reason-codes/{code}`.
