Install

Any language

Connect any server to Penni with plain HTTP: fetch the price, answer 402, verify and settle. Every step with curl.

Every integration, including our own, does the same four steps. Each call authenticates with Authorization: Bearer <API key>; bodies are JSON. Ready-made versions: Python, Go, PHP.

1. Get the price of the path

Terminal
curl -s "https://api.pennipay.eu/v1/sites/$PENNI_SITE_ID/config?path=/premium/report" \
  -H "Authorization: Bearer $PENNI_API_KEY"
Response (shortened)
{
  "x402Version": 2,
  "mode": "test",
  "ready": true,
  "problems": [],
  "blocked": false,
  "match": { "path": "/premium/report", "pattern": "/premium/**", "price": "0.01", "free": false },
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:84532",
      "amount": "10000",
      "asset": "0x808456652fdb597867f38412077A9182bf77359F",
      "payTo": "0xYourWallet",
      "maxTimeoutSeconds": 60,
      "extra": { "name": "EURC", "version": "2" }
    }
  ]
}
  • match.free is true, or accepts is empty: serve the request as usual.
  • blocked is true: the account is out of credits and the site is set to block. Answer 503.
  • Cache the answer per path for up to 60 seconds. The response has an ETag; send it back as If-None-Match.

2. No payment: answer 402

When the request has no PAYMENT-SIGNATURE header, answer 402. The PAYMENT-REQUIRED header is the base64 of this JSON, with accepts copied from step 1:

PAYMENT-REQUIRED (before base64)
{
  "x402Version": 2,
  "error": "PAYMENT-SIGNATURE header is required",
  "resource": { "url": "https://your-site.example/premium/report" },
  "accepts": [ ... ]
}

3. Payment: check it matches

The client retries with PAYMENT-SIGNATURE: base64 JSON with the signed payment and, in accepted, the option it chose. Compare scheme, network, asset, payTo and amount with your current accepts. No exact match: answer 402 again. Never take the amount from the client.

4. Verify and settle

Send the decoded payment and your own copy of the matching option. Verify is free and moves no money; settle puts the payment on-chain.

Terminal
curl -s https://api.pennipay.eu/v1/x402/verify \
  -H "Authorization: Bearer $PENNI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "x402Version": 2, "paymentPayload": { ... }, "paymentRequirements": { ... } }'
# { "isValid": true, "payer": "0x..." }

curl -s https://api.pennipay.eu/v1/x402/settle \
  -H "Authorization: Bearer $PENNI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "x402Version": 2, "paymentPayload": { ... }, "paymentRequirements": { ... } }'
# { "success": true, "transaction": "0x...", "network": "eip155:84532", "payer": "0x..." }

When success is true, serve the content and add the settle response, base64 encoded, as the PAYMENT-RESPONSE header.

Cardano settles after confirmations. Settle may answer errorReason: "settlement_pending": send the identical body once more.

Serving x402 v1 clients

Older v1 clients read the 402 from the JSON body instead of the header, and send their payment in X-PAYMENT. Our Node.js package, the Worker and the WordPress plugin handle both; the examples in these docs serve v2 clients, which is what current x402 SDKs send.