Developer reference

Peptide-Pay API

A REST API for charging cards and crypto, settling to a USDC wallet you control. One POST creates a checkout. One webhook tells you it paid. Ship in under 30 minutes.

REST · JSONBearer authHMAC-SHA256 webhooksIdempotency-KeyCORS-enabled

Getting started

Quickstart (5 min)

Three steps: create a session, redirect the customer, handle the webhook. The sample below is a production-ready Node.js checkout route.

app/checkout/route.ts
// Create a checkout session and redirect your customer.
// Authorization resolves the merchant wallet server-side — no wallet in the body.

const res = await fetch('https://peptide-pay.com/api/v1/checkout/init', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.PEPTIDEPAY_API_KEY}`, // sk_live_…
  },
  body: JSON.stringify({
    amount_cents: 5000,                       // €50.00 — integer, in cents
    currency: 'EUR',                          // EUR, USD, GBP, CAD, AUD, CHF
    email: 'buyer@example.com',               // optional, shown in checkout
    success_url: 'https://mystore.com/success',
    cancel_url:  'https://mystore.com/cart',
    webhook_url: 'https://mystore.com/api/peptidepay-webhook',
    metadata: { order_id: '1234' },
  }),
});

const { id, url, tracking_number } = await res.json();
// => { id: 'cs_abc…', url: 'https://peptide-pay.com/session/cs_abc…',
//      tracking_number: '0x…', provider: 'gateway', status: 'pending', … }

// Redirect your customer to the hosted checkout.
return Response.redirect(url, 303);

That’s the whole happy path. The customer lands on a hosted checkout on , picks card or crypto, and your webhook fires within 30s of payment.

Authentication

Two schemes, depending on the integration mode:

SchemeHowWhen
Bearer tokenAuthorization: Bearer sk_live_…Server-side. Keeps the wallet private.
Wallet in body{ "wallet": "0x…", … }Static sites / widgets with no backend.
Heads up
API keys carry the full identity of the merchant account — treat them like passwords. Never commit them, never ship them to the browser, rotate them from /app/api-keys if leaked.
Tip
No sandbox mode. Signup returns both an sk_live_… and an sk_test_… key. Use sk_live_ as your canonical key — that is what every example here uses. The sk_test_ key is provided for the webhook-receiver simulator at /api/v1/test/fire-webhook. Peptide-Pay settles real on-chain USDC — there is no test network. To dry-run an integration, run a $1 real payment and refund yourself.

API reference

Base URL: . All endpoints speak JSON, return a single object on success, and an object on 4xx/5xx.

POSThttps://peptide-pay.com/api/v1/checkout/init

#Create a checkout session

Mints a hosted checkout URL. The customer opens it, pays with card or crypto, Peptide-Pay settles to your wallet in USDC, webhook fires.

Request body
FieldTypeRequiredDescription
amount_centsintegerrequiredAmount in the smallest unit of currency (cents). Range 100 – 10 000 000.
currencystringrequiredISO 4217 code. Supported: EUR, USD, GBP, CAD, AUD, CHF.
walletstringone-ofUSDC wallet on Polygon (0x + 40 hex). Required unless authenticated via Bearer key.
customer_emailstringoptionalShown in the checkout UI and forwarded to the on-ramp for KYC reuse.
success_urlurloptionalRedirect after successful payment. http/https only.
cancel_urlurloptionalRedirect if the customer abandons checkout.
webhook_urlurloptionalPOST target for order.paid events. Overrides the dashboard default.
providerstringoptionalDefault 'gateway' (smart picker — recommended). Or pin a specific on-ramp id from GET /providers (e.g. moonpay, revolut, banxa, transak).
product_namestringoptionalLabel shown on the checkout page (max 80 chars).
metadataobjectoptionalUp to 10 string key/value pairs, echoed back in the webhook. Reserved key: order_id.
Response (200 OK)
FieldTypeRequiredDescription
idstringoptionalSession id, starts with cs_.
urlstringoptionalHosted checkout URL to redirect the customer to.
statusstringoptionalAlways "pending" at creation.
amountintegeroptionalEcho of amount_cents.
currencystringoptionalEcho of currency.
providerstringoptionalEcho of provider (defaults to 'gateway').
expires_atstringoptionalISO 8601 expiry (24h from creation).
tracking_numberstringoptionalPolygon settlement address — matches address_in in the webhook payload, usable with /track for live monitoring.

Examples

// Node.js 18+
const res = await fetch('https://peptide-pay.com/api/v1/checkout/init', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.PEPTIDEPAY_API_KEY}`,
    'Idempotency-Key': crypto.randomUUID(),   // safe double-submit
  },
  body: JSON.stringify({
    amount_cents: 5000,
    currency: 'EUR',
    email: 'buyer@example.com',
    success_url: 'https://mystore.com/success',
    cancel_url:  'https://mystore.com/cart',
    metadata: { order_id: '1234' },
  }),
});

if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { id, url } = await res.json();
return Response.redirect(url, 303);
Tip
Pass to make retries safe. We replay the cached response for 24h on the same key instead of minting a new session (prevents double-charging on flaky networks).
GEThttps://peptide-pay.com/api/v1/sessions/{id}

#Retrieve a session (polling)

Use this as a webhook fallback, or to hydrate a success page after redirect. Server-side re-checks our settlement layer on every call — cheap (<200ms), so polling every 3-5s is fine.

Response
FieldTypeRequiredDescription
idstringoptionalSession id.
statusstringoptionalpending | paid | expired | failed.
amountintegeroptionalOriginal amount in cents.
currencystringoptionalOriginal currency.
paid_atstring|nulloptionalISO 8601 when the on-chain settlement completed.
paid_providerstring|nulloptionalWhich provider actually processed payment (can differ from the requested one).
txidstring|nulloptionalPolygon settlement txid. Link with polygonscan.com/tx/{txid}.
expires_atstringoptionalISO 8601 expiry.
// Poll every 3-5 seconds until terminal state. Use webhooks for push-
// delivery in production; polling is the fallback when webhooks are down.
async function waitForPayment(sessionId, { timeoutMs = 15 * 60 * 1000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const res = await fetch(`https://peptide-pay.com/api/v1/sessions/${sessionId}`);
    const s = await res.json();
    if (s.status === 'paid')    return s;       // terminal: success
    if (s.status === 'expired') throw new Error('Session expired');
    if (s.status === 'failed')  throw new Error('Payment failed');
    await new Promise(r => setTimeout(r, 4000));
  }
  throw new Error('Polling timeout');
}
GEThttps://peptide-pay.com/api/v1/providers

#Live provider matrix

Lists the on-ramps currently accepting traffic, with per-provider minimum amounts. Cached 5 min at the edge — poll once at app boot, not per request.

Response
FieldTypeRequiredDescription
providers[].idstringoptionalProvider key (passable as `provider` in /checkout/init).
providers[].provider_namestringoptionalHuman label for the dropdown.
providers[].statusstringoptional'active' (always filtered to active on this endpoint).
providers[].minimum_currencystringoptionalISO code of the minimum.
providers[].minimum_amountnumberoptionalLowest amount the provider accepts (in minimum_currency units).
curl -sS 'https://peptide-pay.com/api/v1/providers' | jq '.providers[] | {id, provider_name, minimum_currency, minimum_amount}'

# [
#   { "id": "gateway",  "provider_name": "Smart (recommended)",   "minimum_currency": "USD", "minimum_amount": 1 },
#   { "id": "moonpay",  "provider_name": "Moonpay",               "minimum_currency": "EUR", "minimum_amount": 20 },
#   { "id": "revolut",  "provider_name": "Revolut Ramp",          "minimum_currency": "EUR", "minimum_amount": 10 },
#   { "id": "binance",  "provider_name": "Binance Pay",           "minimum_currency": "EUR", "minimum_amount": 15 },
#   …
# ]
# Cache: 5 minutes at the edge. Call once per deploy, not per request.

Webhooks

When a session reaches a terminal state we POST a signed JSON event to the you configured (per-session or in the dashboard). Always parse the request body for signature verification — re-serializing JSON reorders keys and breaks the HMAC.

POST /your-endpoint  HTTP/1.1
Host: mystore.com
Content-Type: application/json
x-peptidepay-signature: t=1745300551,v1=3f9b5c1e8a7d…    ← HMAC-SHA256, hex

{
  "event":      "order.paid",
  "session_id": "cs_abc123",
  "order_id":   "1234",
  "address_in": "0xAb12…",
  "status":     "paid",
  "amount":     5000,
  "currency":   "EUR",
  "txid":       "0xfa89b2…",
  "paid_at":    "2026-04-23T10:02:31.000Z",
  "attempt":    1
}

Event types

EventWhen
order.paidOn-chain settlement confirmed. + + guaranteed present. Mark the order paid.

Only is delivered today — expired and failed sessions are observable via (status goes after the 24h TTL; terminal failures show ). We may add push events for those in a future release.

Signature verification

Merchants with a signup account receive a secret and every delivery carries an header of the form . Compute and constant-time compare to . Reject anything older than 5 minutes.

Tip
Wallet-only flows (no signup, no ) deliver the webhook unsigned — you should still validate the matches a session you created. For signed deliveries, sign up at /signup to get your secret.
// Node.js — Express/Next.js route handler
import crypto from 'node:crypto';

const SECRET = process.env.PEPTIDEPAY_WEBHOOK_SECRET; // dashboard → Webhooks

export async function POST(req) {
  const rawBody = await req.text();                 // MUST be the raw bytes
  const header  = req.headers.get('x-peptidepay-signature') ?? '';
  const [ tPart, v1Part ] = header.split(',');
  const t  = tPart?.split('=')[1];
  const v1 = v1Part?.split('=')[1];
  if (!t || !v1) return new Response('bad sig', { status: 400 });

  // Reject replays older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300)
    return new Response('stale', { status: 400 });

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  const ok =
    v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
  if (!ok) return new Response('invalid sig', { status: 401 });

  const event = JSON.parse(rawBody);
  // Idempotency: dedupe by event.session_id in your DB — retries re-fire
  // the same event (with an incrementing "attempt" field) until you 2xx.
  if (event.event === 'order.paid') {
    await markOrderPaid(event.order_id, event.txid);
  }
  return new Response('ok');
}
Heads up
Always use the constant-time compare for your language: (Node), (Python), (PHP), (Ruby). A plain leaks the HMAC one byte at a time to a timing attacker.

Retry policy

We retry non-2xx responses (and timeouts > 5s) on an exponential backoff. Six total attempts across ~42 hours:

  • Attempt 1 — immediately on confirmation.
  • Attempt 2 — +5 minutes.
  • Attempt 3 — +15 minutes.
  • Attempt 4 — +1 hour.
  • Attempt 5 — +4 hours.
  • Attempt 6 — +12 hours, then +24 hours (final).

After 6 failed attempts the event is dead-lettered. You can re-request the current state any time via .

Tip
Make your handler idempotent. Dedupe on — the retry may re-fire a paid event you already processed.

Common issues

I see 'invalid signature' on every delivery
Your framework parsed the body as JSON before you hashed it. Read the RAW bytes (Express: express.raw({type:'*/*'}); Next.js: req.text(); Laravel: request()->getContent(); Rails: request.raw_post). Never re-serialize before hashing.
IP whitelist — which IPs do you send from?
Deliveries currently originate from Vercel Edge (dynamic IPs). We don't publish a static range. If you MUST whitelist, use the signature header as your auth gate and accept any source IP — the HMAC is the real identity check.
HTTPS required?
Yes. We refuse to POST to http:// endpoints (confusable deputy / plaintext replay risk). ngrok's free https URL works fine for local testing.
My endpoint is slow — can I extend the 5s timeout?
No. Respond 2xx immediately, then process asynchronously (job queue, setImmediate, goroutine). Long blocking handlers always end up timed out.

SDKs

The API is small enough that is perfectly fine — but the Node SDK gives you types, automatic retries, and a helper that handles signature verification for you.

npm install github:kinerette/peptide-pay-sdk
// npm install github:kinerette/peptide-pay-sdk

import { PeptidePay } from 'peptide-pay';

const pp = new PeptidePay(process.env.PEPTIDEPAY_API_KEY);

// Create a session
const session = await pp.checkout.create({
  amount_cents: 5000,
  currency: 'EUR',
  customer_email: 'buyer@example.com',
  success_url: 'https://mystore.com/success',
  cancel_url:  'https://mystore.com/cart',
  metadata: { order_id: '1234' },
});

// Retrieve a session
const latest = await pp.sessions.retrieve(session.id);

// Verify + parse a webhook (throws on invalid signature)
app.post('/webhooks/peptidepay', express.raw({ type: '*/*' }), (req, res) => {
  const event = pp.webhooks.constructEvent(
    req.body,
    req.headers['x-peptidepay-signature'],
    process.env.PEPTIDEPAY_WEBHOOK_SECRET,
  );
  // event.event === 'order.paid' (currently the only event delivered)
  res.sendStatus(200);
});
Node / TypeScriptstable
peptide-pay

Full types, webhook helper, automatic retries.

Direct fetch()always works
any language

One POST, one GET. No library needed.

Tip
Python, PHP, Ruby, and Go SDKs are on the roadmap. Until they ship, the raw // samples above are the canonical reference — we won’t break them.

Fees

Flat — Peptide-Pay's full commission. No subscription, no monthly fee, no chargeback fees. Card on-ramp fees (~4.5% charged by the upstream card processor) are pass-through — the customer pays them, they never touch your payout.

Payment methodYou payCustomer pays
Card / Apple Pay / Google Pay3%~4.5% (on-ramp, pass-through)
Crypto direct (USDC → USDC)3%gas only (~$0.01 on Polygon)

Full breakdown with worked examples at /fees.

Testing

Every new merchant account gets for free — the full 3% fee is refunded to your wallet within 24h. Use these to rehearse the full flow end-to-end (real card, real USDC, real webhook) before you go live.

  • Sandbox mode is automatic: the first 3 paid sessions per merchant are marked and qualify for auto-refund.
  • MoonPay dev card: , any future expiry, any CVV, ZIP 10001.
  • Local webhook testing: expose localhost with ngrok, paste the URL into the field per session.

Full local loop (ngrok)

# 1. Expose your local webhook endpoint
ngrok http 3000

# 2. Copy the https://xxxx.ngrok-free.app URL and paste it into
#    Dashboard → Webhooks → Endpoint URL, OR send it inline:
curl -X POST 'https://peptide-pay.com/api/v1/checkout/init' \
  -H "Authorization: Bearer $PEPTIDEPAY_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "amount_cents": 100,
    "currency": "EUR",
    "customer_email": "test+sandbox@yours.com",
    "success_url": "https://yours.com/success",
    "cancel_url":  "https://yours.com/cart",
    "webhook_url": "https://xxxx.ngrok-free.app/webhooks/peptidepay"
  }'

# 3. Open the returned `url`, hit MoonPay's dev test card
#    4242 4242 4242 4242 (any future exp, any CVV).
# 4. Your local endpoint receives the signed POST within ~30s of payment.

Errors & rate limits

All errors share the shape . Status codes are standard REST.

400
Invalid JSON or missing field
Body malformed, amount non-numeric, wallet not a 0x address, currency unsupported.
401
Invalid or revoked API key
Bearer token doesn't resolve to a merchant. Rotate it at /app/api-keys.
403
Bad callback signature
Internal — our settlement IPN hit the webhook receiver without the correct per-session signature. Not a merchant-facing error in normal operation.
404
Session not found
Wrong id, or the session has been pruned (> 90d after terminal state).
429
Rate limit exceeded
60 req/min/IP on init, 30 req/min/IP on select. Retry-After header included. Contact support for higher tiers.
502
Upstream unavailable
Settlement network temporarily degraded. Retry in 30s with the same Idempotency-Key. SLA target: 99.5%+.

Troubleshooting

Session is 'paid' in the dashboard but my webhook never fired
Check that webhook_url is reachable over public HTTPS (curl it from outside your LAN). If it's correct, poll GET /sessions/{id} to confirm status — the dashboard /app shows webhook delivery stats (success rate, counts). Six attempts across 42h before dead-letter; you can always re-sync via polling.
HMAC mismatch — signature is always invalid
99% of the time: you're hashing a re-serialized body instead of the raw bytes. Frameworks auto-parse JSON before your handler runs; you need the raw buffer. Next.js: req.text() before any .json(). Express: app.use('/webhooks', express.raw({ type: '*/*' }), …). Rails: request.raw_post. Also check you're computing `HMAC(whsec_secret, t + '.' + rawBody)` — NOT just `HMAC(whsec_secret, rawBody)`. The timestamp prefix is required.
MoonPay says 'service unavailable in your country'
MoonPay restricts ~20 countries (Iran, North Korea, Cuba, full list on their site). Default provider is 'gateway' — the smart picker auto-falls-back to Revolut, Transak, or Banxa which cover different geographies. If you pinned a specific provider with provider: 'moonpay', drop it and let the router choose.
My wallet didn't receive USDC after a 'paid' event
Check polygonscan.com/address/<your-wallet> for USDC (Polygon POS) transfers. Settlement lands 97% to you and 3% to Peptide-Pay - if you don't see the 97% inbound, you may have pasted the wrong wallet into the init call. Re-confirm via GET /sessions/{id} - the txid field points to the real on-chain transfer.
Customer was charged twice
Shouldn't happen. Each session has one settlement addressIn; a second payment to the same address becomes a separate session on our side and we only credit the first one to your order. If it happens, screenshot the two polygonscan txids + the session id and email hi@peptide-pay.com - we refund the duplicate from our treasury.
I get 502 'Payment infrastructure temporarily unavailable'
Our settlement upstream is degraded (< 0.5% of requests). Retry in 30s with the same Idempotency-Key - our cache returns the original response as soon as the wallet mints successfully. Track /status for live incidents.

Ready to integrate?

Most merchants go from zero to their first paid transaction in under 30 minutes.