Skip to main content

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-oid4ac

An 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.