Skip to main content

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

Step 1: install the SDK

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

# Python
pip install oid4pay-oid4ac

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

The 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