Metadata-Version: 2.4
Name: quickcontract
Version: 0.1.0
Summary: The official Python SDK for QuickContract — the new standard for contracts. For humans and AI agents.
Project-URL: Homepage, https://github.com/QuickContractIO/quickcontract-python
Project-URL: Documentation, https://quickcontract.io/docs/api
Project-URL: Repository, https://github.com/QuickContractIO/quickcontract-python
Project-URL: Issues, https://github.com/QuickContractIO/quickcontract-python/issues
Author-email: QuickContract <dev@quickcontract.io>
License: Apache-2.0
License-File: LICENSE
Keywords: agents,ai-agents,anthropic,contracts,digital-signatures,ed25519,langchain,openai,quickcontract,stripe-connect
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: cryptography>=42
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.5
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# quickcontract

The official Python SDK for QuickContract — the new standard for contracts. For humans and AI agents.

Wraps the QuickContract partner API + the agent-layer surfaces (mandate self-discovery, machine-readable terms, signed event ingress, anonymous hash verification) so Python-first agent frameworks (Anthropic Agents SDK, OpenAI Agents SDK, LangChain, plain scripts) can drive contracts end-to-end.

---

## Install

```bash
pip install quickcontract
```

Requires Python 3.9+. Depends on `httpx`, `pydantic` v2, and `cryptography` (for Ed25519 event signatures).

---

## Quick start

### 1. Sync — agent signs a contract

```python
from quickcontract import Client

with Client() as qc:                          # api_key from QC_API_KEY env
    agent = qc.agents.self()                  # mandate self-discovery
    contract = qc.contracts.create(
        "procurement_purchase_order_v1",
        contract_name="PO-2026-Q2-WIDGETS",
        fields={"amount_eur": "12,500.00", "delivery_days": "30"},
        currency="EUR",
        idempotency_key="po-2026-q2-widgets",
    )
    qc.contracts.send(
        contract.id,
        recipient_email="supplier@northwind.com",
        recipient_name="Northwind Supplier",
        party="B",
    )
    signed = qc.contracts.sign_as_agent(contract.id, party="A")
    print(signed.status, qc.contracts.permalink(contract.id)["url"])
```

### 2. Async — agent reports a signed event

```python
import asyncio
from datetime import datetime, timezone
from quickcontract import AsyncClient

async def main():
    async with AsyncClient(agent_private_key="...64-hex-chars...") as qc:
        await qc.events.report(
            "pdf_abc123",
            term_id="term_delivery_check",
            evidence={
                "carrier": "Northwind Logistics",
                "tracking_id": "NW-7841-DE",
                "delivered_at": datetime.now(timezone.utc).isoformat(),
            },
            # signature auto-computed when agent_private_key is set
        )

asyncio.run(main())
```

### 3. Release an escrow milestone

```python
from quickcontract import Client

with Client() as qc:
    qc.escrow.release(
        "pdf_abc123",
        milestone_id="ms_2",
        idempotency_key="release-ms2-2026-05-18",
    )
```

### 4. Anonymous content-hash verification (no API key sent)

```python
from quickcontract import Client

with Client(api_key="qc_anonymous") as qc:
    result = qc.verify.hash("a3f7c9...sha256...")
    if result.found:
        print(f"verified: {result.contract_name}, signers={len(result.signed_by or [])}")
```

---

## Authentication

QuickContract authenticates with a single header. Both styles are accepted:

```
x-api-key: qc_live_<32hex>     # organisation key
x-api-key: qc_agnt_<32hex>     # agent-bound key (Team+)
```

Configure via constructor or environment:

| variable | purpose | default |
| --- | --- | --- |
| `QC_API_KEY` | API key used when none is passed to `Client(...)` | (required) |
| `QC_BASE_URL` | Override the API root (useful for staging / self-host) | `https://quickcontract.io` |
| `QC_AGENT_PRIVATE_KEY_HEX` | 64-hex Ed25519 seed for `events.report` auto-signing | — |

```python
from quickcontract import Client

qc = Client(
    api_key="qc_live_...",
    base_url="https://staging.quickcontract.io",
    timeout=30.0,
)
```

---

## Idempotency

Every state-changing endpoint (POST, PATCH, DELETE) accepts an `idempotency_key=`. Same key + same body within 24 hours returns the cached response — safe to retry on network errors:

```python
qc.contracts.create(
    template_id,
    contract_name="...",
    idempotency_key="po-2026-q2-widgets",      # stable across retries
)
```

The SDK forwards this verbatim as the `Idempotency-Key` header. Generate keys yourself — never let the SDK randomise on retry, otherwise the dedup window does nothing.

---

## Typed exceptions

Every non-2xx response maps to a typed exception that preserves the parsed response body:

| status | exception | useful attributes |
| --- | --- | --- |
| 400 | `ValidationError` | `body` |
| 401 | `AuthenticationError` | — |
| 402 | `PlanGateError` | `feature`, `reason`, `upgrade_to` |
| 403 mandate | `MandateRejected` | `reason`, `code`, `detail`, `agent_id` |
| 403 other | `PermissionDenied` | — |
| 404 | `NotFoundError` | — |
| 409 | `ConflictError` | — |
| 422 | `PredicateRejected` | `reason`, `detail` |
| 429 | `RateLimitExceeded` | `retry_after` (seconds) |
| 5xx | `ServerError` | — |
| other | `QuickContractError` | `status_code`, `body`, `request_id` |

```python
from quickcontract import Client, MandateRejected, PlanGateError

with Client() as qc:
    try:
        qc.contracts.sign_as_agent(contract_id, party="B")
    except MandateRejected as err:
        print(f"agent {err.agent_id} blocked: {err.reason} — {err.detail}")
    except PlanGateError as err:
        print(f"upgrade to {err.upgrade_to} to use {err.feature}")
```

---

## Event signing (machine terms)

Agents report events to programmable terms with an Ed25519 signature over a canonical message:

```
signature = ed25519( SHA-256( termId || "|" || canonicalJson(evidence) || "|" || ISO timestamp ) )
```

Canonical JSON is depth-sorted, no whitespace — must be byte-identical to the backend's `canonicalJson`. The SDK provides:

```python
from quickcontract import sign_event, canonical_json, build_event_message

sig = sign_event(
    term_id="term_delivery_check",
    evidence={"carrier": "Northwind", "weight_kg": 412.7},
    timestamp="2026-05-18T12:00:00Z",
    private_key_hex="...64-hex-chars...",
)
```

When the client is constructed with `agent_private_key=` (or `QC_AGENT_PRIVATE_KEY_HEX` is set), `events.report()` signs automatically.

---

## Webhook signatures

Every webhook delivery sets `X-QC-Signature` to the hex-encoded HMAC-SHA256 of the raw request body. Always verify against the raw bytes — never re-serialise the parsed JSON:

```python
from quickcontract import verify_webhook_signature

@app.post("/webhooks/quickcontract")
def webhook(request):
    raw = request.body                              # bytes, untouched
    sig = request.headers["X-QC-Signature"]
    if not verify_webhook_signature(body=raw, signature_header=sig, secret=WEBHOOK_SECRET):
        return Response(status_code=401)
    event = json.loads(raw)
    # handle event...
```

---

## API surface

| resource | methods |
| --- | --- |
| `client.contracts` | `list`, `get`, `create`, `update`, `delete`, `send`, `sign`, `sign_as_agent`, `status`, `permalink`, `export`, `audit`, `add_term`, `list_recipients`, `add_recipient` |
| `client.events` | `report` |
| `client.escrow` | `release` |
| `client.intelligence` | `analyze`, `get_analysis`, `semantic_diff`, `obligations` |
| `client.templates` | `list`, `get` |
| `client.folders` | `list`, `create` |
| `client.agents` | `self` (requires agent-bound key) |
| `client.verify` | `hash` (anonymous) |
| `client.organization` | `get`, `members` |
| `client.openapi` | `get` |

Both `Client` and `AsyncClient` expose the identical surface. Use whichever fits your runtime; the underlying transport is `httpx`.

---

## Examples

See `examples/`:

- `agent_procurement.py` — end-to-end: agent creates, sends, signs a contract.
- `agent_event_report.py` — agent reports a signed `payload.delivered` event.
- `verify_hash.py` — anonymous content-hash verification.

---

## License

Apache-2.0. See `LICENSE`.
