The official Python SDK for QuickContract — the new standard for contracts. For humans and AI agents.
Project description
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
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
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
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
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)
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 |
— |
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:
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 |
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:
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:
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 signedpayload.deliveredevent.verify_hash.py— anonymous content-hash verification.
License
Apache-2.0. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file quickcontract-0.1.0.tar.gz.
File metadata
- Download URL: quickcontract-0.1.0.tar.gz
- Upload date:
- Size: 24.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfd93ef4bee808bc3a40c388ae4fe3eea36695349e5ab0cb205bb8f00cbf5dc9
|
|
| MD5 |
086a0e23f3eed1064c7bc3ad1dae2001
|
|
| BLAKE2b-256 |
179a2929954eec60dbdbcf8d3ace99cd6f926c362f02de2217dc62cc45701442
|
File details
Details for the file quickcontract-0.1.0-py3-none-any.whl.
File metadata
- Download URL: quickcontract-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2ed24d19ca6b0b9b608f8a611c2e2ce358be7a14296ed8ba6c29928319fb7c9
|
|
| MD5 |
bb502a479deb70c902bfe4cfbc6db981
|
|
| BLAKE2b-256 |
9ac3dc7123734d868cf9ae1527e2283e5bab712d508986b3c85ddc3395ba00d1
|