# SDK usage (Python and TypeScript)

Both SDKs are **thin transports** over the same REST contract. They contain no policy logic and make no decisions;
Quattrino decides. Version **0.31.0**, single file, zero dependencies, **not yet published** to PyPI / npm.

| | Python `quattrino` | TypeScript `@quattrino/sdk` |
|---|---|---|
| get it | `curl -o quattrino.py https://quattrino.io/api/v1/public/sdk/python` or `pip install /path/to/sdk/python` | `curl -o quattrino.ts https://quattrino.io/api/v1/public/sdk/typescript` or `yarn add file:/path/to/sdk/typescript` |
| runtime | Python 3.9+, standard library only (`urllib`) | Node 18+ (global `fetch`), or any runtime with `fetch` |
| construct | `Quattrino(api_key, base_url="https://quattrino.io", timeout=30)` | `new Quattrino({ apiKey, baseUrl?: string, fetch? })` |
| auth header sent | `Authorization: Bearer <key>` + `User-Agent: quattrino-python/0.31.0` | `Authorization: Bearer <key>` + `User-Agent: quattrino-ts/0.31.0` |
| error | `QuattrinoError(status, code, message, details)` | `QuattrinoError { status, code, message, details }` |
| result | `AuthorizationResult` (a `dict` with properties) | `AuthorizationResult` (typed interface) |

## Method map (identical semantics)

| REST | Python | TypeScript |
|---|---|---|
| `POST /authorize` (dry run) | `authorize(amount_minor=, merchant=, purpose=, commitment=, task=, ...)` | `authorize({ amountMinor, merchant, purpose, commitment, task, ... })` |
| `POST /authorize` (binding) | `authorize_binding(..., execution="external", idempotency_key=)` | `authorizeBinding({ ..., execution: "external", idempotencyKey })` |
| `GET /authorizations/{id}` | `get_authorization(id)` | `getAuthorization(id)` |
| `GET /authorizations` | `list_authorizations(status=, limit=)` | `listAuthorizations({ status, limit })` |
| `POST /authorizations/{id}/outcome` | `record_outcome(id, success=, provider=, provider_reference=, actual_amount_minor=, commitment=, proof_id=, ...)` | `recordOutcome(id, { success, provider, providerReference, actualAmountMinor, commitment, proofId, ... })` |
| `POST /authorizations/{id}/revoke` | `revoke(id, reason=)` | `revoke(id, reason)` |
| `GET /authorizations/{id}/explain` | `explain(id)` | `explain(id)` |
| `GET /authorizations/{id}/proof` | `get_proof(id)` | `getProof(id)` |
| `POST /authorizations/verify` (public) | `verify_proof(base, token, audience=)` (module function) / `q.verify_proof(token, audience=)` | `verifyProof(baseUrl, token, { audience })` / `q.verifyProof(token, { audience })` |
| `GET /public/proof-keys` | `proof_keys()` | `proofKeys()` |
| `GET /public/reason-codes[/{code}]` | `reason_codes(family=)`, `describe_reason(code)` | `reasonCodes({ family })`, `describeReason(code)` |
| `POST /intents/compile` | `compile_intent(statement, proposal=, parent_envelope_id=)` | `compileIntent(statement, { proposal, parentEnvelopeId })` |
| `GET /envelopes/{id}`, `GET /envelopes` | `get_envelope(id)`, `list_envelopes()` | `getEnvelope(id)`, `listEnvelopes()` |
| `POST /envelopes`, `/narrow`, `/propose` | `create_envelope`, `narrow_envelope`, `propose_envelope_change` | `createEnvelope`, `narrowEnvelope`, `proposeEnvelopeChange` |
| `GET /authority/me` | `discover(envelope_id=)` | `discover(envelopeId)` |
| `POST /authority/gap` | `gap(**action)` | `gap(action)` |
| `POST /authorizations/{id}/disclosures/{scope}/release` | `release_disclosure(id, scope, recipient=)` | `releaseDisclosure(id, scope, recipient)` |
| `GET /authorize/schema` | `schema()` | `schema()` |
| sandbox execution adapters | `execute(auth, **adapter_args)`, `quote(merchant_key, sku)` | `execute(auth, args)`, `quote(merchantKey, sku)` |

Python keyword arguments are `snake_case` and identical to the REST field names; TypeScript top-level inputs are
`camelCase` (`amountMinor`, `providerReference`) while nested objects (`task`, `attributes`) keep the REST names.
**Responses are the raw REST payloads in both** (`decision`, `effective_maximum_minor`, `denial_reasons`, ...).

## Idempotency defaults

`authorize_binding` / `authorizeBinding` add `Idempotency-Key: qtrn-<uuid>` when you pass none. Pass your own
(`idempotency_key="order-1001"`) for anything you might retry - see [IDEMPOTENCY_AND_RETRIES.md](IDEMPOTENCY_AND_RETRIES.md).
`execute()` adds `qtrn-exec-<uuid>`.

## Python

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

r = q.authorize(amount_minor=1999, merchant="Amazon", purpose="USB-C cable", commitment="ONE_TIME", task={"max_amount_minor": 2000})
r.decision, r.effective_maximum_minor, r.binding_layer          # 'AUTHORIZED', 2000, 'task'
r.reasons                                                        # denial + needs_information + approval reasons, flattened
r.question, r.missing_authority, r.question_required            # NEEDS_INFORMATION / APPROVAL_REQUIRED helpers

auth = q.authorize_binding(amount_minor=1999, merchant="Amazon", purpose="USB-C cable", commitment="ONE_TIME",
                           task={"max_amount_minor": 2000}, execution="external", idempotency_key="order-1001")
auth.authorized, auth.authorization_id, auth.expires_at, auth.execution_package, auth["portable_proof"]["token"]
q.record_outcome(auth.authorization_id, success=True, provider="amazon", provider_reference="114-2233", actual_amount_minor=1999, commitment="ONE_TIME")
q.explain(auth.authorization_id)["plain_language"]

try:
    q.authorize(amount_minor="19.99")
except QuattrinoError as e:
    e.status, e.code, e.message, e.details                       # 422, 'validation_error', ...
```

`authorize(task="buy the best USB-C cable under $20")` with a **string** task compiles it server-side into an envelope first.

## TypeScript

```ts
import { Quattrino, QuattrinoError, verifyProof } from "./quattrino";   // or "@quattrino/sdk" when installed from the tarball
const q = new Quattrino({ apiKey: process.env.QUATTRINO_AGENT_KEY!, baseUrl: "https://quattrino.io" });

const r = await q.authorize({ amountMinor: 1999, merchant: "Amazon", purpose: "USB-C cable", commitment: "ONE_TIME", task: { max_amount_minor: 2000 } });
r.decision; r.effective_maximum_minor; r.effective_authority.binding_layer;

const auth = await q.authorizeBinding({ amountMinor: 1999, merchant: "Amazon", purpose: "USB-C cable", commitment: "ONE_TIME",
                                        task: { max_amount_minor: 2000 }, execution: "external", idempotencyKey: "order-1001" });
await q.recordOutcome(auth.authorization_id, { success: true, provider: "amazon", providerReference: "114-2233", actualAmountMinor: 1999, commitment: "ONE_TIME" });
(await q.explain(auth.authorization_id)).plain_language;
```

## Support matrix and publication status

| | status |
|---|---|
| Python 3.9 - 3.13 | supported (stdlib only) |
| Node 18 / 20 / 22, Deno, Bun | supported (needs global `fetch`) |
| browsers | works technically; **never** ship an agent key to a browser |
| PyPI / npm | **not published**. Local build / install certified in Pass 6 (`QUATTRINO_PASS6_SDK_READINESS.json`). |

The SDKs never embed policy logic, never cache decisions and never retry on their own.
