PayWicket Developer & Merchant Docs

Accept stablecoin payments (USDC on Polygon) with a familiar checkout, automatic on-chain settlement to a wallet you control, signed webhooks, and one-click reconciliation. PayWicket is non-custodial — funds never sit with us.

Base URL: https://paywicket.com

Overview

PayWicket turns a crypto payment into an ordinary checkout. A buyer completes a payment with a wallet or a card; USDC settles directly to the merchant's payout address on Polygon through an audited on-chain router that splits the platform fee automatically; and your system receives a signed payment.confirmed webhook so you can fulfil the order.

How a payment flows

1. Create payment (API) 2. Buyer pays (hosted checkout) 3. On-chain settlement + fee split 4. Confirmation webhook 5. Reconcile / export
  1. Your backend calls POST /api/payments with the USD amount and an optional callbackUrl and idempotencyKey. You get back a payment object with a hosted checkoutUrl.
  2. You redirect the customer to checkoutUrl (or embed it). They pay with a wallet (gasless), a card-funded wallet, or an email-created wallet.
  3. The PaymentRouter contract transfers USDC to the merchant payout address and sends the platform fee to the treasury in the same transaction. A Paid event is indexed and matched to your payment by its paymentRef.
  4. PayWicket marks the payment confirmed and fires a signed payment.confirmed webhook to your callbackUrl.
  5. You reconcile via the webhook, the GET /api/payments/:id read, or the ledger CSV export — each carries the on-chain tx hash and a block-explorer link.

Quickstart

1) Create an account and a wallet in the dashboard — the wallet is issued a scoped API key. 2) Create a payment and redirect the buyer:

curl -X POST https://paywicket.com/api/payments \
  -H "x-api-key: $PAYWICKET_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "amountUsd": 49.99,
    "currency": "USDC",
    "description": "Order #1234",
    "customerEmail": "buyer@example.com",
    "callbackUrl": "https://yourstore.com/webhooks/paywicket",
    "idempotencyKey": "order-1234",
    "metadata": { "orderId": "1234" }
  }'

Response (trimmed):

{
  "id": "pay_9f2c1a8b7d6e5f40",
  "status": "pending",
  "amountUsd": 49.99,
  "currency": "USDC",
  "amountToken": { "human": "49.99", "base": "49990000" },
  "checkoutUrl": "https://paywicket.com/checkout.html?pid=pay_9f2c1a8b7d6e5f40",
  "expiresAt": 1734112800000
}

Send the customer to checkoutUrl. When they pay, you'll receive a payment.confirmed webhook.

Authentication

There are two credential types:

Treat the wallet API key like a password. It authorizes charge creation on your wallet. Keep it server-side; never ship it in browser code.

Payments API

POST /api/payments

Create a payment intent. Authenticate with x-api-key.

FieldTypeNotes
amountUsdnumberRequired. > 0, ≤ 1,000,000. USD value; converted to token at current rate.
currencystringUSDC (default) or MATIC.
descriptionstringShown on checkout; ≤ 200 chars.
customerName, customerEmailstringOptional; stored for reconciliation/receipts.
callbackUrlstringHTTPS endpoint to receive webhooks. Falls back to the wallet's default webhook.
idempotencyKeystringRepeated create with the same key returns the existing payment — safe on retries.
metadataobjectFree-form JSON echoed back on reads and webhooks.

Returns a payment object: id, status, amountUsd, currency, amountToken {human, base}, address, checkoutUrl, paymentUri, qrDataUrl, metadata, createdAt, expiresAt, settlement, and (in on-chain mode) an onchain block with router/token/paymentRef.

GET /api/payments/:id

Fetch current state of a payment (poll as a fallback to webhooks).

GET /api/payments?merchantId=:walletId

List payments for a wallet.

GET /api/config

Public runtime config: active network, chainId, settlement token (symbol/address/decimals), current platformFeeBps, and paymentExpiryMinutes.

Payment states

StateMeaningTransition
pendingCreated, awaiting payment.Initial state.
confirmedFunds received on-chain and matched.From pending when the router Paid event (or balance) is observed. Fires payment.confirmed.
expiredNot paid before expiresAt.From pending after the expiry window (default 30 min).
refundedReturned to the payer.From confirmed via refund. Fires payment.refunded.

Webhooks

PayWicket POSTs a JSON event to your callbackUrl on state changes. Events: payment.confirmed, payment.refunded.

HeaderValue
x-cryptopay-eventThe event type, e.g. payment.confirmed.
x-cryptopay-signatureHMAC-SHA256 hex digest of the raw request body, keyed with your webhook secret.

Body:

{
  "type": "payment.confirmed",
  "sentAt": "2026-08-20T14:03:11.204Z",
  "data": {
    "id": "pay_9f2c1a8b7d6e5f40",
    "status": "confirmed",
    "amountUsd": 49.99,
    "currency": "USDC",
    "metadata": { "orderId": "1234" },
    "settlement": {
      "txHash": "0xabc…",
      "fromAddress": "0x…",
      "amountReceivedBase": "49990000",
      "confirmedAt": 1734112991204,
      "explorerUrl": "https://polygonscan.com/tx/0xabc…"
    }
  }
}

Verify the signature (Node)

import { createHmac, timingSafeEqual } from "node:crypto";

app.post("/webhooks/paywicket",
  express.raw({ type: "application/json" }), (req, res) => {
    const sig = req.get("x-cryptopay-signature");
    const expected = createHmac("sha256", process.env.PAYWICKET_WEBHOOK_SECRET)
      .update(req.body)              // raw bytes, not parsed JSON
      .digest("hex");
    const ok = sig && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!ok) return res.status(400).send("bad signature");

    const evt = JSON.parse(req.body);
    if (evt.type === "payment.confirmed") { /* fulfil order evt.data.metadata.orderId */ }
    res.sendStatus(200);   // respond 2xx within ~5s
});
Reconcile on the id / metadata, and treat webhooks as idempotent — the same event may be delivered more than once. Always confirm state with GET /api/payments/:id before shipping high-value goods.

Reconciliation

Every confirmed/refunded payment carries the on-chain settlement.txHash and an explorerUrl, so your books tie directly to the ledger. Options:

GET /api/merchants/:id/ledger.csv

Columns: date_received, customer_name, customer_email, cryptocurrency_received, amount_crypto, usd_value_at_receipt, wallet_transaction_id, receiving_address, status, refund_tx, description.

Errors & rate limits

Errors return a non-2xx status with a JSON body { "error": "message" }.

StatusMeaning
400Invalid request (e.g. bad amountUsd, missing fields).
401Unknown/missing credential (send x-api-key or bearer token). mfa_required when TOTP is needed.
403Not permitted (e.g. refunding a payment you don't own).
404Resource not found.
429Rate limited. Auth and write endpoints (payments/refunds) are throttled per IP.

Use idempotencyKey on create to make retries safe. Webhook receivers should always return quickly (2xx within ~5s) and process asynchronously.

Payment methods & currencies

Settlement

Settlement is direct and non-custodial. When a payment confirms, the PaymentRouter smart contract, in a single transaction:

There is no T+N payout delay and no pooled balance — funds arrive at the merchant address as the transaction confirms (seconds on Polygon).

Refunds & disputes

POST /api/payments/:id/refund

Authenticate with the account bearer token. Only confirmed payments can be refunded, once. Body accepts an optional toAddress (defaults to the original payer) and, for non-custodial refunds, an txHash you recorded after sending USDC back from your own wallet.

Crypto transactions are final — there are no bank-style forced chargebacks. A "refund" is a merchant-initiated return of USDC. Disputes are handled off-rail (merchant ↔ customer); PayWicket records the refund tx for your books and fires payment.refunded.

Sandbox & testing

PayWicket runs in three modes so you can integrate before touching real funds:

Ask us to provision a sandbox wallet + API key, or self-serve from the dashboard. The recommended acceptance test: create a payment → complete it on the hosted checkout → assert your endpoint received a valid, signature-verified payment.confirmed → reconcile it.

Integration options

Merchant technical requirements

Questions or need a sandbox key? Email juan.lavieri@gmail.com. Some deeper architecture material (settlement internals, custom routing, commercial terms) is shared under NDA.