Authentication
Authenticate every server-to-server request with your API key as a bearer token. Keys are issued from the
merchant dashboard after registration.
Treat keys like passwords — rotate immediately on compromise.
Authorization: Bearer zp_live_…
Content-Type: application/json
Public endpoints (/healthz, /api/status, embed asset routes) do not require auth.
Idempotency
All POST writes accept an
Idempotency-Key header (≤128 chars). Replays return the original response
with the original status. Use a fresh UUID per logical operation, persist it alongside the call, and re-send on retry.
Idempotency-Key: 9f4d1c20-7d3b-4e0a-9c0f-3f9b2a8b1234
Errors
Errors return a non-2xx status and a stable JSON envelope. The
code field is machine-readable; message is human-readable
and may change. Never branch on message.
{
"error": {
"code": "invalid_amount",
"message": "Field \"amount\" must be a positive number"
}
}
| Status | Code family | Meaning |
| 400 | invalid_* | Request validation failed. |
| 401 | unauthorized | Missing or invalid API key. |
| 403 | forbidden | Key lacks access to this resource. |
| 404 | not_found | Merchant, payment, or invoice does not exist. |
| 405 | method_not_allowed | HTTP verb not supported on this path. |
| 409 | conflict | Idempotency key reused with a different payload. |
| 429 | rate_limited | Per-key rate limit exceeded — see below. |
| 5xx | internal / upstream | Retry with exponential backoff. |
Rate limits
Sliding window per API key. Limits are advertised in response headers and apply per endpoint family.
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1715900400
Free tier: 60 req/min per key. Beta cohorts and self-hosted deployments raise the ceiling — contact us for production limits.
Merchants/api/merchants/*
POST
/api/merchants/register
Register a merchant. Returns a deterministic merchant id, an API key handle, and your webhook receipt URL. Idempotent.
Request body
| Field | Type | Notes |
namerequired | string | Merchant display name, ≤120 chars. |
walletAddressrequired | string | Base58 Solana pubkey where USDC settles. Wallet-less — no connect required. |
emailrequired | string | Operational notices, magic-link login. |
webhookUrloptional | string | HTTPS URL. Must serve a valid certificate. |
Example
curl -X POST https://zettapay.dev/api/merchants/register \
-H "Authorization: Bearer zp_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Acme Robotics",
"walletAddress": "4Nd1mYG6X9wQ…ZkR2",
"email": "ops@acme.example",
"webhookUrl": "https://acme.example/zp/webhook"
}'
POST
GET
/api/merchants/onboard
Self-service onboarding state used by /signup. POST creates a merchant from the signup flow,
GET returns the checklist + dashboard link + ready-to-paste embed snippet.
GET
/api/merchants/:merchant
Public merchant card: handle, wallet, accepted currencies. Used by the embed widget to resolve data-merchant.
GET
/api/merchants/:merchant/analytics
TPV, MRR, conversion, top customers, and 30-day trends. Powers the per-merchant dashboard. Aggregates only — no PII.
GET
/api/merchants/:merchant/payments
Paginated payment history scoped to a single merchant. Pair with ?limit= and ?cursor=.
| Query | Default | Notes |
limit | 50 | Max 200. |
cursor | — | Opaque, returned in next. |
status | — | pending · confirmed · failed |
GET
/api/merchants/:merchant/payouts
Settled batches with on-chain signatures. Powers the /dashboard/payouts page.
POST
GET
DELETE
/api/merchants/:merchant/keys
Rotate, list, or revoke API keys. Newly issued keys are returned once in cleartext; persist immediately.
PATCH
/api/merchants/:merchant/settings
Update webhook URL, email, brand color, default currency. Audit-logged.
Payments/api/pay · /api/payments
POST
/api/pay
Create a payment intent. Returns a Solana Pay URI and an invoice id. The payer scans the QR (or pastes the address) from any Solana wallet —
no wallet connect, no extension. Settlement is observed on-chain and dispatched as a webhook.
Request body
| Field | Type | Notes |
merchantIdrequired | string | ≤64 chars. |
amountrequired | number | USDC, positive, ≤1,000,000. |
currencyoptional | string | Default USDC. |
payerWalletoptional | string | Base58 Solana pubkey. Used to scope the invoice + populate analytics. |
metadataoptional | object | Free-form JSON returned on webhook. Treat as semi-public. |
Response
{
"id": "inv_01H8XK…",
"status": "pending",
"amount": 10,
"currency": "USDC",
"checkoutUrl": "https://zettapay.dev/checkout/inv_01H8XK…",
"solanaPayUri": "solana:4Nd1mYG6X9wQ…?amount=10&spl-token=…&reference=…",
"expiresAt": "2026-05-16T20:30:00Z"
}
GET
/api/payments
Account-wide payment listing for the authenticated key. Use the per-merchant variant for narrower scopes.
GET
/api/analytics/:merchant
Aggregate timeseries: daily TPV, average ticket, distinct payers, conversion. JSON output, ready for charting.
POST
/api/simulate/:merchant
Devnet-only. Emits a synthetic settlement, fires the merchant webhook, and inserts a row into payments. Convenient for end-to-end webhook tests without touching the network.
WebhooksStripe-grade delivery
Delivery semantics
Webhooks are POSTed to the URL registered on the merchant. We retry up to three times with exponential backoff (10s, 60s, 5m).
Deliveries are queued, ordered per invoice, and replayable from the dashboard for 90 days. Webhook URLs must be HTTPS.
Event payload
{
"id": "evt_01H8XK…",
"type": "payment.confirmed",
"created": "2026-05-16T19:42:11Z",
"data": {
"invoiceId": "inv_01H8XK…",
"merchantId": "@acme-robotics",
"amount": 10,
"currency": "USDC",
"payer": "6tK…dpL",
"signature": "5x9c…Wq3",
"metadata": { "orderId": "o_42" }
}
}
Event types
| Event | Fires when |
payment.pending | Invoice created. |
payment.confirmed | On-chain confirmation observed. |
payment.failed | Invoice expired or amount mismatch. |
payout.settled | Daily batch landed in merchant wallet. |
merchant.updated | Settings changed via dashboard or API. |
Signature verification
Each delivery includes an X-ZettaPay-Signature header: t=<timestamp>, v1=<hex>.
Compute HMAC-SHA256(t + "." + rawBody, webhookSecret) and constant-time compare against v1.
Reject deliveries older than 5 minutes to defeat replay.
import { createHmac, timingSafeEqual } from 'crypto';
export function verify(rawBody: string, header: string, secret: string) {
const [tPart, vPart] = header.split(',').map(p => p.trim());
const t = tPart.split('=')[1];
const v1 = vPart.split('=')[1];
const mac = createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
if (mac.length !== v1.length) return false;
return timingSafeEqual(Buffer.from(mac), Buffer.from(v1));
}
Replay protection
Every event ships with a stable id. Persist id on receipt and ignore re-deliveries.
Combine with the timestamp window to defeat replay attacks. Replays from the dashboard are deliberate and re-use the original event id.
AI Agentsx402 · MCP
x402 — autonomous payments
ZettaPay is an x402
payment provider. When an agent hits a paywalled resource that returns 402 Payment Required, the response advertises a
ZettaPay challenge. The agent signs a transaction blob (held in its own wallet, never custodied by ZettaPay) and re-sends the request with
X-PAYMENT. The protocol verifies on-chain and unlocks the resource.
HTTP/1.1 402 Payment Required
WWW-Authenticate: X402 realm="ZettaPay",
amount=0.05, currency="USDC",
recipient="4Nd1mYG…ZkR2",
nonce="a3f9…"
Full spec lives at /docs/quickstart#x402.
POST
/api/mcp
JSON-RPC 2.0
Model Context Protocol endpoint. Exposes payment primitives as tool calls so any MCP-aware host (Claude, GPT, Gemini) can issue invoices,
poll status, and stream confirmations without bespoke integration code.
Tools advertised
| Tool | Description |
create_invoice | Mint an invoice and return the Solana Pay URI. |
get_invoice | Fetch status + on-chain signature for an invoice id. |
list_payments | Paginate confirmed payments for the calling agent identity. |
resolve_merchant | Look up a merchant card by handle or wallet. |
Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_invoice",
"arguments": { "merchantId": "@acme", "amount": 0.05 }
}
}
Optional integrationsthird-party · BYO
Third-party fiat onramp
ZettaPay does not ship a built-in fiat onramp. The protocol is non-custodial: the customer must already hold the
cryptocurrency they pay with. If a fraction of your audience needs fiat→crypto, embed any onramp you trust
(MoonPay,
Ramp,
Transak,
Coinbase Onramp,
etc.) alongside the ZettaPay widget — pass the invoice's destination address to the onramp's deposit-address parameter so the
purchased crypto lands at the merchant's wallet.
ZettaPay's chain listener detects the deposit on-chain just like any other payment and posts your standard
payment.confirmed webhook — there is no special onramp event type. KYC, fees, currencies, and regional
restrictions are governed entirely by the onramp provider.
Health & statusunauthenticated
GET
/healthz
Liveness probe. 200 when the process is reachable.
GET
/ready
Readiness probe. 200 only when DB, RPC, and webhook dispatcher are all reachable.
GET
/metrics
Prometheus exposition format. Per-endpoint latency histograms, request counts, webhook delivery counters.
GET
/api/status
Component-by-component health snapshot used by the public /status page. Includes an RSS feed at /status/feed.rss.
POST
/api/faucet
devnet only
Drip devnet USDC to a wallet you control. Rate-limited per IP. See /docs/faucet.
Ready to ship?
Start with the 5-minute quickstart or drop the embed onto your checkout.