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.

Not published yet. The packages are not on npm, PyPI or the Go module proxy yet, so the install commands on this page do not work today. They show the names the packages will ship under.

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.