Merchant quickstart (10 minutes)
Add OID4Pay to your existing storefront so agentic checkouts settle to your own Stripe Connect account. You will register a merchant identity, publish a signed Offer, and verify both the Offer signature and the presented mandate at request time. The merchant SDK is verify-only: it confirms offers, mandates, and catalogs. It never charges. Settlement is performed by the Authorization Server over HTTP after verification succeeds.
Prerequisites
- A Stripe Connect account (the settlement rail).
- A storefront with a route per SKU (the Offer URL).
- One Ed25519 keypair for signing offers; the public JWK is published at
/.well-known/jwks.jsonon your origin.
Step 1: install the SDK
# Node / TypeScript
npm install @oid4pay/oid4ac-merchant
# Python
pip install oid4pay-oid4ac
# Go
go get github.com/oid4pay/oid4ac-goThe three SDKs share a single conformance harness; a Node verifier accepts the byte-exact output of a Python or Go signer.
Step 2: publish the JWKS
Host your public JWK at https://<origin>/.well-known/jwks.json:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"kid": "merchant-2026-05-14",
"alg": "EdDSA",
"use": "sig",
"x": "<base64url public key bytes>"
}
]
}Step 3: register with the Discovery directory
curl -sS https://discover.oid4pay.com/v1/merchants \
-H "content-type: application/json" \
-d '{
"display_name": "Alpacanica",
"domain": "shop.alpacanica.com",
"country": "NL",
"currencies_accepted": ["EUR"],
"categories": ["apparel", "home"],
"jwks_uri": "https://shop.alpacanica.com/.well-known/jwks.json",
"catalog_uri": "https://shop.alpacanica.com/.well-known/oid4ac-catalog",
"oid4ac_acceptance_methods": ["card", "sepa_debit"],
"stripe_connect_account_id": "acct_<your acct>"
}'The directory verifies the JWKS reachability and DNS ownership before
flipping verification_status=verified. Provisional entries are
not surfaced to agents.
Step 4: serve signed Offers
Each SKU has a route that emits a JSON-LD Offer body plus the RFC 9421
Signature-Input + Signature headers. Your storefront signs the offer with
the Ed25519 private key whose public half you published in Step 2. The
merchant SDK does not sign; it verifies. Use canonicalOfferDigest to compute the same canonical SHA-256 over the body that the verifier
computes, so the Content-Digest header you emit matches:
import { canonicalOfferDigest } from "@oid4pay/oid4ac-merchant";
const body = {
"@context": "https://schema.org",
"@type": "Offer",
"sku": "test-pinata",
"name": "Test Pinata",
"amount_minor": 1299,
"currency": "EUR",
"in_stock": true,
};
// base64url SHA-256 over the canonical JSON of the body.
const digest = canonicalOfferDigest(body);
// Emit it as the Content-Digest header value: `sha-256=:${digest}:`
// (digest is already base64url-encoded; do not re-encode it). Then sign the
// RFC 9421 signature base with your Ed25519 private key.Step 5: verify offer + mandate at request time
When an agent presents a mandate, verify your own Offer signature and the SD-JWT VC mandate before you act. Both calls are pure verification; neither moves money. The mandate presentation is the whole compact string (SD-JWT VC, disclosures, then KB-JWT), passed as a single argument; the merchant audience is the second argument.
import { verifyOffer, verifyMandate } from "@oid4pay/oid4ac-merchant";
export async function POST(request) {
const body = await request.json();
// 1. Verify your own offer signature (defence in depth; an agent could
// present a tampered offer).
const offer = await verifyOffer(body.offer, body.offerHeaders, ownJwks, {
expectedTargetUri: `https://shop.alpacanica.com/products/${body.offer.sku}`,
});
// 2. Verify the SD-JWT VC mandate + KB-JWT against the AS JWKS. The
// presentation is the full compact string; the audience is positional.
const mandate = await verifyMandate(body.presentation, "https://shop.alpacanica.com", {
jwksUrl: "https://as.oid4pay.com/.well-known/jwks.json",
expectedIssuer: "https://as.oid4pay.com",
expectedOfferDigest: offer.bodyDigest,
});
// Verification passed. mandate.amountMinor / mandate.currency / mandate.cnfJkt
// are now trustworthy. Settlement (the charge) is performed by the
// Authorization Server over HTTP, not by this SDK.
return Response.json({ ok: true, mandateId: mandate.mandateId });
}Step 6: handle SSF events
The AS emits SSF (Shared Signals Framework) events for revocations, mandate updates, and disputes. Subscribe with your registered receiver URL:
curl -sS https://as.oid4pay.com/ssf/subscribe \
-H "content-type: application/json" \
-d '{
"delivery_method": "https://schemas.openid.net/secevent/risc/delivery-method/push",
"delivery_endpoint": "https://shop.alpacanica.com/ssf/receive",
"events_requested": [
"oid4ac.mandate.revoked",
"oid4ac.payment.disputed"
]
}'Next steps
- Read the mandate wire shape for the verifier rules.
- Wire a CDN-side drop-in instead of a server route: see cdn-bundle.
- Subscribe to the changelog for breaking wire-version bumps.