The OID4Pay wire reference
This page is the authoritative reference for every wire shape an OID4Pay implementation must emit or accept. If your code and this page disagree, this page wins. Each subsection pins one shape; the SDKs and the authorization server are tested against them, so an implementation that matches this page interoperates with the rest of the network.
The shapes are grouped into four domains: discovery (where bootstrapping starts), core endpoints, token mechanics, and cryptography, credentials, and security. Each shape opens with one sentence on why the rule exists; the wire detail follows.
The happy path
The standard lifecycle runs in one direction. A client first discovers the authorization server (1.1), pushes an authorization request (2.1), and the principal approves it in the Wallet Portal (2.5). The AS returns a single-use code (3.5) the client exchanges for a key-bound access token (3.1, 3.2) and an SD-JWT VC mandate (4.4). The agent presents these to the merchant, which verifies them, optionally introspects the token (2.3) and checks the status list (2.4), then settles the charge. Either side can revoke (2.2) at any point. The diagram below traces one pass.
- Wallet / agent pushes the authorization request to the authorization server (PAR).
- The authorization server returns 201 with a request_uri.
- The wallet / agent sends the authorize request; the principal consents.
- The authorization server redirects (302) with a code and iss.
- The wallet / agent exchanges the code at the token endpoint with a DPoP proof.
- The authorization server returns a JWT-AT (cnf.jkt) and an SD-JWT VC mandate.
- The wallet / agent charges the merchant with the mandate, JWT-AT, DPoP, and KB-JWT.
- The merchant introspects the token with the authorization server.
- The authorization server replies active plus the claims.
- The merchant returns a payment_intent_id and a signed receipt.
1. Discovery & bootstrapping
1.1 Authorization-server metadata
Start here: every endpoint URL, supported algorithm, and key location is published by the
authorization server, so a client discovers the network instead of hard-coding it. The AS serves
its metadata at /.well-known/oauth-authorization-server (RFC 8414); it names the PAR, token, revoke,
and introspection endpoints, the JWKS location, and the algorithms each surface accepts. A client
SHOULD prefer that document over hard-coding any URL below.
2. Core endpoints
2.1 PAR response
Pushing the authorization request server-side keeps its parameters out of the browser address bar, where they could be logged or tampered with. The Pushed Authorization Request (RFC 9126) is the only authorization channel. The AS responds:
HTTP/1.1 201 Created
Content-Type: application/json
Cache-Control: no-store
{
"request_uri": "urn:ietf:params:oauth:request_uri:abc...",
"expires_in": 60
}2.2 /oauth/revoke
Revocation is the kill switch: it lets a wallet or principal retire a token the moment a device or session is suspected compromised. RFC 7009 revocation endpoint. Accepts both access and refresh tokens; revocation cascades to the entire token family on a refresh-token revoke.
2.3 /oauth/introspect
Introspection lets a merchant confirm a token is still live and read its claims at charge time,
rather than trusting a possibly-stale copy. RFC 7662 introspection endpoint. Requires private_key_jwt client authentication; returns the full JWT-AT claim set plus active.
2.4 W3C VC Status List v1.0
A single signed status list lets merchants check whether a mandate has been revoked without calling
the AS on every charge. The AS publishes a Status List 2021 VC at /oauth/status-list.
Merchants check the bit index named in each mandate's credentialStatus claim before
settling a charge.
2.5 OIDC contract (Wallet Portal)
The Wallet Portal signs human users in with standard OIDC, so the same identity layer that
authenticates people also anchors the mandates agents act under. The Wallet Portal acts as an OIDC
RP against the AS. Required scopes: openid, email, profile, wallet:read, wallet:write. The id_token carries nonce, iss, aud, sub, auth_time, and acr claims.
3. Token mechanics
3.1 JWT-AT claim set (typ=at+jwt)
The access token is self-describing: a merchant can validate it offline against the AS public key and read every claim it needs to authorize a charge. It is an RFC 9068 JWT-AT with mandatory claims:
{
"iss": "https://as.oid4pay.com",
"sub": "principal_id_b64url",
"aud": "https://shop.example.com",
"client_id": "client_abc",
"jti": "uuid-v4",
"exp": 1730000600,
"iat": 1730000000,
"nbf": 1730000000,
"scope": "payment.charge",
"cnf": { "jkt": "base64url-sha256-of-dpop-public-jwk" },
"mandate_id": "mandate_xyz",
"agent_client_id": "client_abc"
}The header is typ=at+jwt, alg=EdDSA. The algorithm whitelist (4.1) lists the accepted values.
3.2 DPoP nonces vs (jkt, jti) replay
DPoP binds the token to a key the client holds, so a stolen token is useless to anyone without the
matching private key; the replay window stops a captured proof being reused. Every protected
endpoint requires a DPoP proof (RFC 9449). The AS issues a fresh DPoP-Nonce on every
4xx and randomly on successful responses. Replay protection is keyed by (jkt, jti) with a 5-minute window in Redis; replays return invalid_dpop_proof.
3.3 Resource indicators / audience
Audience binding stops a token issued for one merchant being replayed at another: each token names
exactly one origin. Per RFC 8707, the PAR and token requests carry a resource parameter
pinning the audience. The issued aud claim on the JWT-AT is the verbatim resource
value; merchants reject any JWT-AT whose aud does not match their origin.
3.4 Refresh token contract
Rotating refresh tokens turns token theft into a detectable event: a stolen token used after the legitimate client has rotated it reveals the compromise. Refresh tokens are opaque random strings (32 bytes b64url). Refresh rotates on every exchange; the prior refresh token is revoked server-side. A second use of a rotated token triggers a cascade revocation of the entire token family (RFC 6749 + OAuth 2.1 BCP).
3.5 Authorization code single-use + cascade revoke
Single-use codes with a short life close the window for an intercepted code to be redeemed; a replay is treated as compromise. Authorization codes are single-use, 60-second TTL. A second use of a consumed code revokes the issued token family.
4. Cryptography, credentials & security
4.1 Algorithm whitelist + alg=none rejection
Pinning an explicit algorithm per surface and refusing alg=none blocks the classic JWT downgrade attack, where an attacker strips the signature or swaps in a
weaker algorithm. Universal MUST: refuse alg=none. Per-surface whitelists:
| Surface | Accepted | Refused |
|---|---|---|
| JWT-AT | EdDSA | everything else |
| ID Token | EdDSA | everything else |
| client_assertion | EdDSA | everything else |
| DPoP proof | EdDSA, ES256 | HMAC; everything else |
| SD-JWT VC mandate | EdDSA | everything else |
| Signed offer (RFC 9421) | ed25519, ecdsa-p256-sha256 | HMAC; none; everything else |
4.2 RFC 9421 HTTP message signatures
Signing the HTTP message itself lets a merchant prove an offer or catalog was not altered in transit. Signed offers and signed catalogs both use RFC 9421. The signature input MUST cover at least:
"@method" "@target-uri" "@authority" "content-type" "content-digest"created and expires are mandatory; the accepted alg values are ed25519 and (server- side only) ecdsa-p256-sha256. The Content-Digest header carries the SHA-256 of the JSON-LD body.
4.3 private_key_jwt assertion
Clients authenticate with a signed assertion instead of a shared secret, so there is no long-lived
password to leak; the single-use jti stops the assertion being replayed. Client
authentication uses RFC 7523 private_key_jwt. The assertion aud MUST be the AS token endpoint URL; iss and sub are both the client_id; jti is single-use against the same Redis replay store.
4.4 SD-JWT VC mandate + KB-JWT
Selective disclosure lets the wallet prove only the claims a charge needs (cap, currency,
allowlist) without revealing the principal's full identity (data minimisation); the Key Binding JWT
proves the holder is presenting it, live, to this merchant. The mandate VC is signed by the AS over
a principal-bound claim set. Selective disclosure carries minimum claims: mandate_id, principal_id, spend_cap_minor, currency, merchant_allowlist, not_before, not_after. The Key Binding JWT (KB-JWT) the wallet emits at charge time binds the
disclosure to aud=merchant_origin and nonce=sha256(merchant_nonce || offer_digest).