Skip to main content

Bring your own agent

The MCP server is a convenience, not a requirement. OID4Pay never inspects what an agent is: the Authorization Server authenticates a registered client by its key, and authorises a payment against a mandate the human approved. Any runtime that can sign and make HTTP requests can hold that client identity, whatever framework it is written in.

This page is the wire contract. If you are building on Node and want the flow driven for you, start at the agent quickstart instead.

What your runtime has to be able to do

No secret is shared with OID4Pay. Both keypairs are generated on your side and only the public halves are registered. Store the private halves the way you would any signing key.

Step 1: pair the agent with a human

An agent is meaningless on its own. It becomes somebody's agent when it registers with a one-shot setup token that the human minted from their wallet session. The wallet shows this token as a code and a QR payload; carry it to your agent however suits your runtime.

Register with the token, your public keys, and the redirect URIs you will use. The token is single-use and expires.

POST /oauth/register HTTP/1.1
Host: as.oid4pay.com
Content-Type: application/json

{
  "setup_token": "<code from the wallet>",
  "client_name": "My agent",
  "token_endpoint_auth_method": "private_key_jwt",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "jwks": { "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "..." } ] },
  "dpop_jwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
}

The response carries your client_id. The Authorization Server binds it to the human who minted the token, which is what makes later mandates theirs and not yours.

Redirect URI rules worth knowing before you get a 400:

Step 2: ask for a mandate

Every payment starts by pushing the request to the Authorization Server. Authenticate the call with a private_key_jwt assertion, attach a DPoP proof, and describe the payment you want authorised.

{
  "type": "oid4ac_mandate",
  "amount_minor": 499,
  "currency": "EUR",
  "merchant": "https://shop.example.com",
  "offer_digest": "<canonical digest of the signed offer>",
  "line_items": [{ "sku": "socks-01", "qty": 1 }],
  "consent_mode_hint": "scan"
}

Send that as the authorization_details entry alongside response_type=code, code_challenge_method=S256, a state, the resource naming the merchant, and the openid oid4ac:payment scope. The narrower payment:initiate scope is not enough to settle: the merchant refuses the payment call without oid4ac:payment.

The reply is a request_uri with a short lifetime. Move on to the authorisation step promptly; a stale request URI is gone.

Step 3: let the human approve

Call the authorisation endpoint with the request URI. What comes back depends on the consent mode the deployment and the human's policy resolve to. With cross-device approval, which is the mode that suits an agent with no browser, you get a pending response carrying an approval code and the wallet URL to show.

Show the code or its QR. The human opens their wallet, sees the amount, currency, merchant, and line items, and approves or declines. Meanwhile, poll the request URI's status endpoint until it reports approval, then continue. Poll at a human pace, a couple of seconds apart, and give up after a bounded wait rather than spinning.

Step 4: exchange the code

Exchange the authorisation code with a fresh DPoP proof whose key matches the one you used at the push step, your PKCE verifier, and another private_key_jwt assertion. You get back an access token bound to your DPoP key, a refresh token, and the mandate.

The mandate is a selectively-disclosable credential. It always carries the merchant audience, the validity window, and a confirmation claim naming your DPoP key. The amount, currency, merchant, line items, and offer digest are disclosures you present alongside it.

Step 5: pay

Presenting the mandate to the merchant takes four things:

  1. The offer. Fetch the merchant's signed offer and compute its canonical digest. This has to equal the offer_digest you bound at step 2, which means you fetch the offer before asking for the mandate, not after.
  2. A single-use nonce from the merchant.
  3. A key-binding JWT over the presentation, carrying that nonce and signed with the same DPoP key the mandate names.
  4. The presentation itself: the issued credential, the disclosures you are revealing, and the key-binding JWT, joined by ~.

The merchant re-verifies all of it against the Authorization Server's published keys before it charges anything: the issuer signature, the audience, the window, the offer digest, the nonce, and the key binding. None of those checks are advisory.

What the mandate does and does not bound

A mandate authorises one amount, at one merchant, for one holder key, inside one time window. It is not a card, a balance, or a standing authority. Revoking it, or the token family it belongs to, stops the agent immediately; the human can do that from their wallet at any time.

Note what that does not cover. A mandate is not by itself a spending budget across purchases, and per-deployment limits and policy checks are separate controls an operator may or may not have enabled. If you are designing an agent that spends repeatedly, put your own ceiling in your runtime rather than assuming the mandate is one.

Next steps