Documentation
API reference
Everything needed to create payment links, read their status and confirm settlement from your own backend.
Quick start
The shortest path: sign in with a wallet, create a link, share it, then poll verification until it settles.
# 1. challenge
curl -X POST https://paynova-alpha.vercel.app/api/auth/nonce
# 2. sign the EIP-4361 message with your merchant wallet,
# then exchange it for a session cookie
curl -X POST https://paynova-alpha.vercel.app/api/auth/verify \
-H "content-type: application/json" \
-c cookies.txt \
-d '{"message":"...","signature":"0x..."}'
# 3. create a payment link
curl -X POST https://paynova-alpha.vercel.app/api/links \
-H "content-type: application/json" -b cookies.txt \
-d '{"chainId":4663,"assetId":"4663:usdg","amount":"250.00"}'
# 4. after your customer pays, verify the hash
curl -X POST https://paynova-alpha.vercel.app/api/links/<id>/verify \
-H "content-type: application/json" \
-d '{"txHash":"0x..."}'Authentication
Merchant endpoints require a session cookie obtained by signing an EIP-4361 message. Public endpoints — reading a link and verifying a payment — need no session.
- POST /api/auth/nonce
- Issues a single-use nonce valid for ten minutes.
- POST /api/auth/verify
- Body: message, signature. Sets droppy_session on success. The address comes from the verified signature, never from the body.
- GET /api/auth/session
- Returns the signed-in address, or null.
- POST /api/auth/logout
- Clears the session cookie.
The message must use the exact statement returned by the nonce endpoint, your real host as the domain, and a chain id the server supports. Anything else is rejected.
Create a link
POST /api/links — session required.
{
"chainId": 4663,
"assetId": "4663:usdg",
"amount": "250.00",
"invoiceId": "1042",
"description": "Design retainer",
"ttlMinutes": 1440
}{
"id": "cr9q6NmpVnYiyetO",
"merchantAddress": "0xE0a4...048A",
"chainId": 4663,
"assetId": "4663:usdg",
"amount": "250.00",
"amountBaseUnits": "250000000",
"invoiceId": "1042",
"status": "waiting",
"createdAt": "2026-09-09T02:14:02.812Z",
"expiresAt": "2026-09-10T02:14:02.812Z"
}- chainId
- 4663 (mainnet) or 46630 (testnet). Required.
- assetId
- "4663:eth" or "4663:usdg". Required.
- amount
- Decimal string. Must be positive, and must not carry more decimal places than the asset supports — 18 for ETH, 6 for USDG.
- invoiceId
- Optional, up to 64 characters. Control characters are stripped.
- description
- Optional, up to 200 characters.
- ttlMinutes
- Optional. 0 to 43200 (30 days). Defaults to 1440. 0 means never expires.
- merchantAddress
- Not accepted. The receiving address always comes from your session — a value here is ignored.
Read a link
GET /api/links/:id — public, so a payer can load the checkout. GET /api/links lists your own links and needs a session.
Verify a payment
POST /api/links/:id/verify — public and idempotent. This is the only call that can move a link to paid.
{
"ok": true,
"status": "paid",
"txHash": "0x8f3a...c41d",
"from": "0x1c9d...77aE",
"paidBaseUnits": "250000000",
"blockNumber": "57938764",
"confirmations": 4,
"link": { "status": "paid", "...": "..." }
}Repeat calls with the same hash return the same result. Do not fulfil an order on a 202 (confirming) — wait for status paid.
Statuses
- waiting
- Created, nothing seen on chain yet.
- confirming
- A transaction is mined or pending but below the confirmation threshold.
- paid
- Receipt succeeded, recipient and amount verified, confirmations cleared.
- failed
- Reverted, underpaid, or paid to a different address.
- expired
- The link passed its expiry without a verified payment.
Errors
- 400
- Validation failed. The error field explains exactly which input and why.
- 401
- No valid session. Sign in with a wallet signature.
- 404
- No such payment link.
- 409
- That transaction already settled a different link, or this link is already paid by another hash.
- 410
- The link has expired.
- 422
- Verification failed. The reason field explains what the chain actually showed.
- 429
- Rate limited. Retry-After gives the seconds to wait.
Rate limits
- POST /api/auth/nonce
- 20 per minute
- POST /api/auth/verify
- 10 per minute
- POST /api/links
- 30 per minute, per merchant
- GET /api/links/:id
- 120 per minute
- POST /api/links/:id/verify
- 60 per minute
- PUT /api/merchant
- 30 per minute, per merchant
Self-hosting
Droppy runs on Next.js with any PostgreSQL-compatible database. Four environment variables matter in production.
- DATABASE_URL
- PostgreSQL connection string. Run the migration before serving traffic.
- SESSION_SECRET
- 32+ random characters. The app refuses to start in production without it.
- TRUST_PROXY
- Set to true behind a proxy so rate limiting sees the real client IP.
- RPC_URL_4663
- A dedicated RPC endpoint. Server-only — never exposed to the browser.
Ready to integrate?
Create a link in the dashboard, then reproduce it with one POST.