x402 / payment flow
/api/verify is gated by an HTTP 402 micropayment proof (x402). The provider stamps a canonical quote — asset, network, amount, payTo — and the partner responds with an X-PAYMENT header carrying a base64url JSON proof. On the docs surface we surface the canonical 402 envelope and, when configured, a SANDBOX-ONLY signed proof so a partner evaluator can copy the literal header into their client.
The 402 envelope
On a missing or invalid X-PAYMENT header, /api/verify returns 402 with a JSON body of shape { resource, accepts: [{ asset, network, amount, payTo, facilitator? }] } plus a base64url-encoded JSON envelope stamped into the PAYMENT-REQUIRED header. The header is a machine-readable superset of the body — it carries the request's transactionId so the partner can match the envelope to the original POST without re-decoding.
The X-PAYMENT proof
The partner replies with an X-PAYMENT header carrying the proof: a base64url-encoded JSON { transactionId, sender, payTo, amount, signature }. The signature is HMAC-SHA256 over the canonical string {transactionId}|{sender}|{payTo}|{amount}, keyed by the verification token secret. The server re-derives the signature (timingSafeEqual) and confirms sender === payTo, then discards the proof body — only the proof's paymentId and settledAt stamp the row.
Replay guard
When settlement succeeds, the row is stamped with the x402PaymentId returned by the facilitator. A re-submission carrying the same paymentId on a verified row is rejected with a 409 + the canonical 402 envelope (same shape as a missing-header 402), plus an `error: "payment_id_already_settled"` discriminator so the partner can split a missing-proof 402 from a replay 409 in client code.
Request
POST/api/verify (gated by x402)
Body: { transactionId: string (1-200), platform?: string [a-z0-9_-]{1,80} }. Headers: X-PAYMENT — base64url JSON envelope with { transactionId, sender, payTo, amount, signature }. `sender` is the EVM address the partner controls; it must equal `payTo` (USDC quote is sender-bound to this /api/verify resource).
Body / parameters
{
"transactionId": "tx_3f8a1b22c4d5e6f7"
}x402 integration
Code examples
Use curl when you only need to inspect the initial 402 challenge. Use Node.js when you want to retain the challenge payload, request the sandbox-only proof, and retry /api/verify with X-PAYMENT for the 200 receipt path.
curl · 402 challenge
curl -i -sS -X POST https://provably-2.polsia.app/api/verify -H 'content-type: application/json' -d '{"transactionId":"tx_3f8a1b22c4d5e6f7"}'Node.js · 402 → 200 receipt
(async () => {
const baseUrl = 'https://provably-2.polsia.app';
const payload = { transactionId: 'tx_3f8a1b22c4d5e6f7' };
// First call: retain the 402 challenge body and PAYMENT-REQUIRED header.
const challengeResponse = await fetch(baseUrl + '/api/verify', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
const challengeBody = await challengeResponse.json();
const paymentRequired = challengeResponse.headers.get('PAYMENT-REQUIRED');
console.log('402 challenge', {
status: challengeResponse.status,
paymentRequired,
body: challengeBody,
});
// The sandbox returns { transactionId, sender, payTo, amount, signature }.
const proofResponse = await fetch(baseUrl + '/api/docs/x402', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
});
const proofBody = await proofResponse.json();
const proofHeader = proofBody?.xPayment?.header;
if (typeof proofHeader !== 'string' || !proofHeader.startsWith('X-PAYMENT: ')) {
throw new Error('The sandbox proof is not configured; the 200 retry is unavailable.');
}
// Illustrative wire shape only (not executable):
// X-PAYMENT: <redacted>
const xPayment = proofHeader.slice('X-PAYMENT: '.length);
const receiptResponse = await fetch(baseUrl + '/api/verify', {
method: 'POST',
headers: {
'content-type': 'application/json',
'X-PAYMENT': xPayment,
},
body: JSON.stringify(payload),
});
const receiptBody = await receiptResponse.json();
console.log('200 verify receipt', {
status: receiptResponse.status,
body: receiptBody,
});
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});The proof stays transaction-bound to the same payload in both calls; keep real signatures and payment credentials out of client code and logs. Read the x402 payment and replay notes ↗
Response
402 (gated), 200 (verified), 409 (replay), 400, 500
402 body: { resource: "/api/verify", accepts: [{ asset: "USDC", network: "base", amount, payTo, facilitator? }] }. 402 header: `PAYMENT-REQUIRED: <base64url({resource, transactionId, accepts})>`. 200 body: { status, verificationId, transactionId, token? }. 409 body: same canonical 402 envelope + error: "payment_id_already_settled".
Embedded sandbox
Fire a same-origin call against /api/docs/x402 and read the canonical wire shape back. No external tools, no auth — the docs surface is public.
transactionId ready
Awaiting call. Send a request to render the wire shape back.
Ready to integrate the real endpoint?
Request a sandbox API key →