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
curl -s "https://api.pennipay.eu/v1/sites/$PENNI_SITE_ID/config?path=/premium/report" \ -H "Authorization: Bearer $PENNI_API_KEY"
{
"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.freeis true, oracceptsis empty: serve the request as usual.blockedis true: the account is out of credits and the site is set to block. Answer503.- 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:
{
"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.
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.
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.