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.
Getting started
Peptide-Pay has 4 integration stacks (Shopify, WooCommerce, AI builders, direct API). This page documents the auth mode — same for all 4. For the visual stack overview, see /integrate.
Drop-in button, no signup. Pass your USDC wallet address in the request body. Zero backend state; public, visible in DevTools.
Server-to-server. Hide your wallet behind Bearer sk_live_. Configure branding, webhooks, mass payouts from the dashboard.
Ready-made plugin. Upload the ZIP, paste your API key + webhook secret, orders auto-complete on payment. HPOS-ready, WC 7.0+.
Custom App + Manual Payment Method. ~30 min install on an existing store. We mark orders paid via Shopify Admin API after settlement.
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.
// 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:
| Scheme | How | When |
|---|---|---|
| Bearer token | Authorization: Bearer sk_live_… | Server-side. Keeps the wallet private. |
| Wallet in body | { "wallet": "0x…", … } | Static sites / widgets with no backend. |
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.
#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.
| Field | Type | Required | Description |
|---|---|---|---|
| amount_cents | integer | required | Amount in the smallest unit of currency (cents). Range 100 – 10 000 000. |
| currency | string | required | ISO 4217 code. Supported: EUR, USD, GBP, CAD, AUD, CHF. |
| wallet | string | one-of | USDC wallet on Polygon (0x + 40 hex). Required unless authenticated via Bearer key. |
| customer_email | string | optional | Shown in the checkout UI and forwarded to the on-ramp for KYC reuse. |
| success_url | url | optional | Redirect after successful payment. http/https only. |
| cancel_url | url | optional | Redirect if the customer abandons checkout. |
| webhook_url | url | optional | POST target for order.paid events. Overrides the dashboard default. |
| provider | string | optional | Default 'gateway' (smart picker — recommended). Or pin a specific on-ramp id from GET /providers (e.g. moonpay, revolut, banxa, transak). |
| product_name | string | optional | Label shown on the checkout page (max 80 chars). |
| metadata | object | optional | Up to 10 string key/value pairs, echoed back in the webhook. Reserved key: order_id. |
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | optional | Session id, starts with cs_. |
| url | string | optional | Hosted checkout URL to redirect the customer to. |
| status | string | optional | Always "pending" at creation. |
| amount | integer | optional | Echo of amount_cents. |
| currency | string | optional | Echo of currency. |
| provider | string | optional | Echo of provider (defaults to 'gateway'). |
| expires_at | string | optional | ISO 8601 expiry (24h from creation). |
| tracking_number | string | optional | Polygon 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);#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.
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | optional | Session id. |
| status | string | optional | pending | paid | expired | failed. |
| amount | integer | optional | Original amount in cents. |
| currency | string | optional | Original currency. |
| paid_at | string|null | optional | ISO 8601 when the on-chain settlement completed. |
| paid_provider | string|null | optional | Which provider actually processed payment (can differ from the requested one). |
| txid | string|null | optional | Polygon settlement txid. Link with polygonscan.com/tx/{txid}. |
| expires_at | string | optional | ISO 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');
}#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.
| Field | Type | Required | Description |
|---|---|---|---|
| providers[].id | string | optional | Provider key (passable as `provider` in /checkout/init). |
| providers[].provider_name | string | optional | Human label for the dropdown. |
| providers[].status | string | optional | 'active' (always filtered to active on this endpoint). |
| providers[].minimum_currency | string | optional | ISO code of the minimum. |
| providers[].minimum_amount | number | optional | Lowest 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
| Event | When |
|---|---|
| order.paid | On-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.
// 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');
}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 .
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
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);
});peptide-payFull types, webhook helper, automatic retries.
any languageOne POST, one GET. No library needed.
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 method | You pay | Customer pays |
|---|---|---|
| Card / Apple Pay / Google Pay | 3% | ~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.
400401403404429502Troubleshooting
- 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.