Skip to content

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.

end-to-end
# 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.

Verify a payment

POST /api/links/:id/verify — public and idempotent. This is the only call that can move a link to paid.

200 OK
{
  "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.

Current network status

Ready to integrate?

Create a link in the dashboard, then reproduce it with one POST.