Python SDK: oid4pay-oid4ac
The Python SDK runs on CPython 3.10+ (3.10, 3.11, 3.12, and 3.13). It
pins joserfc for SD-JWT VC and JWT decode, cryptography for offer signature verification, and httpx for fetching JWKS documents. It is a verify-only
library: it validates mandates, offers, and catalogs. It does not
settle payments and does not sign offers.
Install
pip install oid4pay-oid4ac
# or
uv pip install oid4pay-oid4acAn optional OpenTelemetry adapter ships under the otel extra: pip install "oid4pay-oid4ac[otel]".
API reference
verify_offer(offer_body, signature_headers, merchant_jwks, *,
expected_target_uri=None)
Verifies an RFC 9421 signed offer against the merchant's JWKS. Pass the
decoded JSON-LD body, an OfferSignatureHeaders instance,
and the merchant JWKS dict. Returns a VerifiedOffer.
Raises OfferVerifyError with a code attribute
that mirrors the Node, Go, and CDN SDKs.
verify_mandate(presentation, merchant_audience, *, jwks=None,
jwks_url=None, jwks_cache=None, expected_issuer=None,
expected_nonce=None, expected_offer_digest=None)
Verifies an SD-JWT VC mandate presentation. The first argument is the
single compact presentation string (issuer JWS plus disclosures plus
KB-JWT); the second positional argument is the merchant audience URL
that the mandate must be bound to. Supply the issuer JWKS through one
of the keyword-only sources: a pre-fetched jwks dict, a jwks_url the SDK fetches over HTTPS, or a jwks_cache paired with jwks_url. Pass expected_nonce to enforce KB-JWT nonce equality and expected_offer_digest to enforce the disclosed digest.
Returns a VerifiedMandate, a frozen dataclass with a to_dict() method exposing the disclosed claims. Raises MandateVerifyError on any failure.
fetch_jwks(jwks_url)
Synchronous HTTPS GET of a JWKS document, with retry and backoff. Returns the parsed JSON dict. It takes a JWKS URL, not a merchant identifier, and it is not a coroutine: do not await it.
Catalog helpers
The SDK also ships verify_catalog and serve_catalog for signed product catalogs, plus the
selection helpers pick_cheapest_matching and pick_in_stock, and canonical_offer_digest for
computing an offer digest locally.
Example: FastAPI storefront route
from fastapi import APIRouter, HTTPException
from oid4pay_oid4ac import (
verify_offer,
OfferSignatureHeaders,
OfferVerifyError,
fetch_jwks,
)
router = APIRouter()
# fetch_jwks is synchronous and takes the merchant's JWKS URL.
MERCHANT_JWKS = fetch_jwks("https://shop.example.com/.well-known/oid4ac-jwks.json")
@router.post("/verify-offer")
def verify(req: OfferRequest) -> dict:
headers = OfferSignatureHeaders(
signature_input=req.signature_input,
signature=req.signature,
content_digest=req.content_digest,
target_uri=f"https://shop.example.com/oid4ac/offer/{req.sku}",
)
try:
v = verify_offer(
req.offer_body,
headers,
MERCHANT_JWKS,
expected_target_uri=f"https://shop.example.com/oid4ac/offer/{req.sku}",
)
except OfferVerifyError as exc:
raise HTTPException(status_code=400, detail=exc.code)
# Verification succeeded. The merchant now hands the verified offer
# to its own checkout flow. Settlement is performed by the
# Authorization Server over HTTP, never by this SDK.
return {"keyid": v.keyid, "offer_digest": v.body_digest}Example: verify a mandate
from oid4pay_oid4ac import verify_mandate, MandateVerifyError
try:
mandate = verify_mandate(
presentation,
"https://shop.example.com",
jwks_url="https://as.oid4pay.com/.well-known/jwks.json",
expected_nonce=pending_nonce,
expected_offer_digest=published_offer_digest,
)
except MandateVerifyError as exc:
raise ValueError(exc.code)
# mandate is a frozen dataclass; persist its claims via to_dict().
audit_row = mandate.to_dict()Algorithm whitelist
Identical to the Node SDK: ed25519 and ecdsa-p256-sha256 for signed offers; EdDSA for
mandate (SD-JWT VC and KB-JWT) verification. Every decode runs against
a fixed allowlist, so alg=none and HMAC substitution are
refused.