Endpoint reference

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".

Try it

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 →