Skip to main content

Hello, OID4Pay

This page walks an agent author through a complete OID4Pay testmode payment in under ten minutes. By the end you have:

Sandbox: the public sandbox at sandbox.oid4pay.com is being finalised. When it opens, sandbox tokens are throwaway and capped at 100 charges per hour per client.

Step 1: install an SDK

Pick whichever SDK matches your stack:

# Node / TypeScript
npm install @oid4pay/oid4ac-merchant

# Python
pip install oid4pay-oid4ac

# Go
go get github.com/oid4pay/oid4ac-go

Step 2: register a sandbox client

Sandbox client registration uses RFC 7591 dynamic registration. POST a minimal JSON body to /oauth/register:

curl -sS https://sandbox.oid4pay.com/oauth/register \
  -H "content-type: application/json" \
  -d '{
    "client_name": "my-agent-prototype",
    "token_endpoint_auth_method": "private_key_jwt",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "jwks": {"keys": [/* your client public JWK */]}
  }'

The response carries your client_id. Store it; the secret counterpart is the private key whose public JWK you just published.

Step 3: request a pushed authorization (PAR)

Every authorization request goes through PAR. The response carries a request_uri with a one minute TTL. See the PAR response for the exact shape.

curl -sS https://sandbox.oid4pay.com/oauth/par \
  -H "content-type: application/x-www-form-urlencoded" \
  -d "client_id=$CLIENT_ID" \
  --data-urlencode "client_assertion=$CLIENT_ASSERTION_JWT" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "response_type=code" \
  -d "scope=payment.charge" \
  -d "code_challenge=$PKCE_S256" \
  -d "code_challenge_method=S256" \
  --data-urlencode "redirect_uri=https://my-agent.example.com/callback"

The private_key_jwt assertion is the only client authentication method; the algorithm whitelist pins EdDSA as the only accepted algorithm. The PAR response shape is on the spec page.

Step 4: redirect the principal

Send the principal to the wallet portal at https://wallet.oid4pay.com/authorize?request_uri=$REQUEST_URI. The wallet authenticates the principal, displays the mandate-binding consent screen, and returns to your redirect_uri with a one-time authorization code.

Step 5: exchange the code for a DPoP-bound JWT-AT

curl -sS https://sandbox.oid4pay.com/oauth/token \
  -H "DPoP: $DPOP_PROOF_JWT" \
  -H "content-type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=$CODE" \
  -d "code_verifier=$PKCE_VERIFIER" \
  -d "client_id=$CLIENT_ID" \
  --data-urlencode "client_assertion=$CLIENT_ASSERTION_JWT" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  --data-urlencode "redirect_uri=https://my-agent.example.com/callback"

The response carries an access_token with typ=at+jwt (see the JWT-AT claim set) and an id_token if the OIDC scope was requested. The DPoP proof binds the token to your keypair via the cnf.jkt claim.

Step 6: settle a testmode charge

Send the signed offer + the DPoP-bound JWT-AT to the merchant's verify- and-charge endpoint. The merchant SDK wraps both calls in a single helper:

// Node / TypeScript
import { verifyOffer, charge } from "@oid4pay/oid4ac-merchant";

const verification = await verifyOffer(offerBody, signedOfferHeaders, merchantJwks, {
  expectedTargetUri,
});
const result = await charge({
  accessToken,
  dpopKey,
  offerDigest: verification.bodyDigest,
  idempotencyKey: crypto.randomUUID(),
});

Successful charges return a charge_id, a stripe_payment_intent_id (the current settlement rail), and the mandate_id the charge debited.

Next steps