Developers
Developers
This page describes the API of the Payzilla devnet preview, so you can see how a seller and a buyer talk to it. The API runs on a private staging environment, which we switch on when we test and scale down when it is idle, and there is no public base URL yet. Merchant keys and seller wallets are set up by us. If you want to run the flow, get in touch or read how to try the preview.
Reference for the devnet preview. A private staging environment exists; there is no public base URL yet. Names and shapes may change.
Overview
There are three roles. A buyer, often an AI agent, wants a resource. A seller runs the API or tool that charges for it. Payzilla acts as the payment facilitator: it creates the payment challenge, verifies and settles the signed payment on Solana devnet, and records everything in one ledger. For the plain-English story, see how a payment works. The preview uses x402 version 2, the exact scheme, on Solana devnet, with a test token.
Authentication
- Send your merchant key in a header named
x-api-key. The value is the key itself, with no prefix. - Send a unique
Idempotency-Key(up to 255 characters) when you create an intent. The same key with the same body returns the same intent. The same key with a different body returns 409. - A key belongs to one merchant. Keys are random, shown once and stored only as a hash. Every read runs as that merchant, and another merchant’s payments return 404.
Endpoints
| Method and path | Auth | What it does |
|---|---|---|
GET /healthz | None | Liveness check. Returns ok true with 200, or ok false with 503. |
GET /supported | Merchant key | The payment kinds supported (x402 v2, scheme exact, Solana devnet) and the network-fee payer address. |
POST /verify | Merchant key | Checks a signed payment against the payment requirements. Read-only. Returns isValid, an invalid reason if any, and the payer. |
POST /settle | Merchant key | Settles a verified payment. Idempotent. It waits up to 10 seconds for the payment to be released; otherwise it returns success false with the reason settlement_pending and a transaction signature. |
POST /v1/intents | Merchant key and Idempotency-Key | Creates a payment intent and returns the intent id, its status, an expiry and the payment challenge to send to the buyer. |
GET /v1/payments | Merchant key | Lists your payments, newest first. Query: limit (1 to 200, default 50) and before (a cursor). |
GET /v1/payments/:id | Merchant key | One payment with its attempts, timeline and ledger entries. |
POST /v1/checkout/sessions | Merchant key and Idempotency-Key | Card rail, mock card provider only. Also POST /v1/checkout/sessions/:id/confirm and GET /v1/checkout/sessions/:id. |
Every response is sent with Cache-Control: no-store. Errors have the form {"error":"<code>"}, sometimes with a message.
Create a payment intent
curl -X POST https://<base-url>/v1/intents \
-H "x-api-key: <MERCHANT_KEY>" \
-H "Idempotency-Key: <unique-id>" \
-H "content-type: application/json" \
-d '{"amount":"2000","resourceUrl":"https://api.example.com/report"}'
The amount is a string of atomic units with 6 decimals, so "2000" is 0.002 test USDC. The maximum is 1,000 test USDC.
amount(required): atomic units, digits only.resourceUrl(optional): up to 2,048 characters.description(optional): up to 500 characters.payTo(optional): must be one of your registered seller wallets.
The response is 201 for a new intent and 200 for a replay. It contains intentId, status, expiresAt and paymentRequired. An intent lives for 10 minutes.
The payment challenge
paymentRequired holds x402Version 2, a resource with the URL and description, and one entry in accepts:
scheme:exact.network: Solana devnet.asset: the devnet test token.amount: atomic units.payTo: the seller’s registered wallet.maxTimeoutSeconds: 60.extra.feePayer: the network-fee payer.extra.memo: the intent id.extra.paymentFlow:upfront.
The seller answers its buyer with HTTP 402, the challenge in the body and the same challenge, base64-encoded, in a PAYMENT-REQUIRED header. The buyer retries with a PAYMENT-SIGNATURE header.
Seller flow
- When a request arrives without a payment, call
POST /v1/intentsand answer 402 with the returned challenge. - When the buyer retries with
PAYMENT-SIGNATURE, callPOST /verifywith{x402Version, paymentPayload, paymentRequirements}. - Call
POST /settlewith the same body. - Serve the resource only when
/settlereturnssuccess: truewith a transaction signature. If it returnssettlement_pending, answer 402 with a Retry-After and let the buyer resend the same header. - Reconcile with
GET /v1/payments.
Our demo resource server implements this flow as middleware. It is reference code, not a packaged SDK, and it serves only GET requests.
What the buyer sees from the demo resource server
| Response | Meaning and what to do |
|---|---|
| 402 with a payment challenge | No payment yet. Pay and retry with the PAYMENT-SIGNATURE header. |
| 402 settlement_pending | Settlement is still completing. Wait for Retry-After and resend the same signature. Do not sign again. |
| 402 payment_invalid | The payment was rejected before anything was submitted. A reason is included. |
| 402 payment_failed | The payment did not complete. |
| 409 already_served, payment_in_flight, unknown_intent | The payment was already used, a different payload is in flight, or the intent is unknown. |
| 429 rate_limited | Too many requests. Wait for Retry-After. |
| 503 payments_unavailable, facilitator_unavailable, settlement_indeterminate | Temporary problem. Resend the same signature if you already signed one. |
Agent (buyer) flow
- Request the resource and read the challenge from the
PAYMENT-REQUIREDheader. - Check the price, the budget and the recipient against your own limits. Our demo agent also refuses anything that is not devnet.
- Sign an exact-scheme payment with the official x402 Solana client. Store the signed header before you send it.
- Request again with
PAYMENT-SIGNATURE. - On
settlement_pending, resend the same header after Retry-After. Do not sign a second payment for the same request. - Success is a 200 with a
PAYMENT-RESPONSEheader.
Statuses
- Attempt: pending, authorized, submitted, captured, final. The exits are failed, voided (card only), reverted and refunded. Failed, voided, reverted and refunded are terminal.
- Intent: created, requires_action, processing, succeeded, failed, canceled, reverted.
succeededmeans released, not merely captured.
What to gate fulfilment on: payment_intent.succeeded, payment_attempt.releasable, or a /settle result of success: true. Do not gate on a bare captured, which can appear before release.
Release: an amount at or below the seller’s threshold is released at captured, and a larger amount at final. The default is 5 test USDC per seller, with a platform ceiling of 50 test USDC. The default serving mode is settle-then-serve. A seller can opt in to serve-then-settle for low-value routes, with an exposure cap.
Webhooks
Events include payment_intent.created, processing, succeeded, failed, canceled and reverted, and payment_attempt.created, authorized, submitted, captured, releasable, final, failed, voided, reverted and refunded, plus reconciliation.break_opened when our records and the network or provider disagree. The body is {id, type, apiVersion, createdAt, data}, where data holds the intent id, attempt id, rail and event details.
- Headers:
AFI-Event-Id(stable across retries),AFI-Delivery-Attempt(starting at 1) andAFI-Signature, in the formt=<unix seconds>,v1=<hex>. - The signature is a hex HMAC-SHA256, keyed with your endpoint secret, over the string
<t>.<raw request body>. - To verify: read the raw body, parse
tandv1, recompute the HMAC, compare in constant time, reject atthat is too old, and de-duplicate onAFI-Event-Id. Return 2xx quickly. - Delivery is at least once. Failed deliveries retry with a backoff that starts at 1 second and doubles up to 1 hour.
- Webhook destinations must be https on port 443 at a public address. Redirects are not followed.
The payment object
Each item in GET /v1/payments has: id, status, amount and amountDecimal, asset, channel, rail (x402 or card), attemptStatus, transaction (the Solana signature, or null), releasable, createdAt, payer (null until claimed), idempotencyKey, networkFee (null until recorded), platformFee (null: no fee is recorded today), refunded (true once a card payment has been refunded) and gasPaidBy. Card payments add fields such as the card brand and last four digits, 3-D Secure result and provider reference. The detail route adds the attempts, the timeline and the ledger entries.
Errors
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Missing, unknown or revoked key. |
merchant_inactive | 403 | The merchant account is suspended. |
invalid_request, invalid_json, invalid_amount, amount_too_large | 400 | The request body is malformed or the amount is out of range. |
idempotency_key_required | 400 | POST /v1/intents needs an Idempotency-Key header. |
idempotency_conflict | 409 | The same Idempotency-Key was used with a different body. |
not_found | 404 | Unknown route or payment, including another merchant’s payment id. |
payload_too_large | 413 | The body is larger than 64 KiB. |
rate_limited | 429 | More than 300 write requests per minute for one merchant. Retry-After: 60. |
too_many_concurrent_settles | 429 | More than 8 settlements in flight for one merchant. Retry-After: 2. |
gas_unavailable | 503 | The network-fee account is unable to pay, so no challenge is issued. Retry-After: 5. |
internal_error | 500 | Unexpected error. |
Protocol outcomes, such as an invalid payment, come back as an x402 body with HTTP 200 and an isValid or success flag.
Limits
- Request body: 64 KiB.
- Write requests: 300 per minute per merchant. Concurrent settlements: 8 per merchant.
- Settle wait: up to 10 seconds, then
settlement_pending. - Amount: up to 1,000 test USDC. Intent lifetime: 10 minutes.
Coming
- A public base URL and a packaged SDK.
- Self-service merchant keys, key rotation and webhook endpoint registration, with signing-secret rotation.
- Seller wallet registration over the API.
- Refunds and non-delivery claims for x402.
- An error catalog with request ids.
- EVM chains, and real money only after the money path is reviewed.
Payzilla is software, not a bank. This is a devnet preview with test tokens only. See the roadmap and how the system is built on the security page.
Found a bug in the preview? Write to [email protected].