API Peptide-Pay
Una API REST per accettare carte e crypto, con settlement su un wallet USDC che controlli tu. Un POST crea un checkout. Un webhook ti dice che ha pagato. In produzione in meno di 30 minuti.
Inizia qui
Peptide-Pay supporta quattro modalità di integrazione. Scegli quella che ti calza meglio; l'API sotto è la stessa.
Bottone drop-in, niente signup. Passa il tuo indirizzo wallet USDC nel body della richiesta. Zero stato backend; pubblico, visibile nei DevTools.
Server-to-server. Nasconde il tuo wallet dietro Bearer sk_live_. Configura branding, webhook, payout di massa dalla dashboard.
Plugin pronto. Carichi lo ZIP, incolli chiave API + webhook secret, gli ordini si completano in automatico al pagamento. HPOS-ready, WC 7.0+.
Custom App + Metodo di Pagamento Manuale. ~30 min di installazione su uno store esistente. Marchiamo gli ordini come pagati tramite Shopify Admin API.
Quickstart (5 min)
Tre step: crea una sessione, redirect il cliente, gestisci il webhook. L'esempio sotto è una route di checkout Node.js production-ready.
// 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);Questo è tutto l'happy path. Il cliente arriva su un checkout hosted su , sceglie carta o crypto, e il tuo webhook parte entro 30s dal pagamento.
Autenticazione
Due schemi, a seconda della modalità di integrazione:
| Schema | Come | Quando |
|---|---|---|
| Bearer token | Authorization: Bearer sk_live_… | Server-side. Tiene il wallet privato. |
| Wallet nel body | { "wallet": "0x…", … } | Siti statici / widget senza 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.Riferimento API
Base URL: . Tutti gli endpoint parlano JSON, restituiscono un singolo oggetto in caso di successo, e un oggetto su 4xx/5xx.
#Crea una sessione di checkout
Genera un URL di checkout hosted. Il cliente lo apre, paga con carta o crypto, Peptide-Pay fa settlement sul tuo wallet in USDC, parte il webhook.
| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
| amount_cents | integer | obbligatorio | Importo nell'unità minima della valuta (centesimi). Range 100 – 10 000 000. |
| currency | string | obbligatorio | Codice ISO 4217. Supportati: EUR, USD, GBP, CAD, AUD, CHF. |
| wallet | string | uno-di | Wallet USDC su Polygon (0x + 40 hex). Obbligatorio se non autenticato con Bearer key. |
| customer_email | string | opzionale | Mostrato nella UI del checkout e passato all'on-ramp per riuso KYC. |
| success_url | url | opzionale | Redirect dopo pagamento riuscito. Solo http/https. |
| cancel_url | url | opzionale | Redirect se il cliente abbandona il checkout. |
| webhook_url | url | opzionale | Target POST per gli eventi order.paid. Sovrascrive il default della dashboard. |
| provider | string | opzionale | Default 'gateway' (smart picker — consigliato). Oppure fissa un on-ramp specifico da GET /providers (es. moonpay, revolut, banxa, transak). |
| product_name | string | opzionale | Label mostrato sulla pagina di checkout (max 80 caratteri). |
| metadata | object | opzionale | Fino a 10 coppie stringa chiave/valore, restituite nel webhook. Chiave riservata: order_id. |
| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
| id | string | opzionale | Session id, inizia con cs_. |
| url | string | opzionale | URL di checkout hosted su cui mandare il cliente. |
| status | string | opzionale | Sempre "pending" alla creazione. |
| amount | integer | opzionale | Echo di amount_cents. |
| currency | string | opzionale | Echo di currency. |
| provider | string | opzionale | Echo di provider (default 'gateway'). |
| expires_at | string | opzionale | Scadenza ISO 8601 (24h dalla creazione). |
| tracking_number | string | opzionale | Indirizzo di settlement Polygon — coincide con address_in nel payload del webhook, usabile con /track per il monitoring live. |
Esempi
// 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);#Recupera una sessione (polling)
Usalo come fallback del webhook, o per idratare una pagina di successo dopo il redirect. Server-side ricontrolla il nostro layer di settlement a ogni chiamata — costa poco (<200ms), quindi il polling ogni 3-5s va bene.
| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
| id | string | opzionale | Session id. |
| status | string | opzionale | pending | paid | expired | failed. |
| amount | integer | opzionale | Importo originale in centesimi. |
| currency | string | opzionale | Valuta originale. |
| paid_at | string|null | opzionale | ISO 8601 di quando il settlement on-chain è andato a buon fine. |
| paid_provider | string|null | opzionale | Quale provider ha effettivamente processato il pagamento (può differire da quello richiesto). |
| txid | string|null | opzionale | Txid di settlement su Polygon. Linka con polygonscan.com/tx/{txid}. |
| expires_at | string | opzionale | Scadenza ISO 8601. |
// 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');
}#Matrice provider live
Lista gli on-ramp che stanno accettando traffico, con importi minimi per provider. Cached 5 min all'edge — fai polling una volta al boot, non per richiesta.
| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
| providers[].id | string | opzionale | Chiave provider (da passare come `provider` in /checkout/init). |
| providers[].provider_name | string | opzionale | Label human-readable per il dropdown. |
| providers[].status | string | opzionale | 'active' (sempre filtrato su active in questo endpoint). |
| providers[].minimum_currency | string | opzionale | Codice ISO del minimo. |
| providers[].minimum_amount | number | opzionale | Importo minimo accettato dal provider (in unità di minimum_currency). |
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.Webhook
Quando una sessione raggiunge uno stato terminale facciamo POST di un evento JSON firmato all{webhookUrl} che hai configurato (per-sessione o in dashboard). Parsa sempre il body {raw} della richiesta per verificare la firma — ri-serializzare il JSON riordina le chiavi e rompe lHMAC.
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
}Tipi di evento
| Evento | Quando |
|---|---|
| order.paid | Settlement on-chain confermato. + + garantiti presenti. Segna l'ordine come pagato. |
Oggi viene consegnato solo — le sessioni expired e failed sono osservabili via (lo status diventa dopo il TTL di 24h; i failure terminali mostrano ). Potremmo aggiungere eventi push per quelli in una release futura.
Verifica firma
I merchant con un account signup ricevono un secret e ogni delivery porta un header nella forma . Calcola e confronta constant-time con . Rifiuta tutto ciò che è più vecchio di 5 minuti.
// 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');
}Policy di retry
Facciamo retry delle risposte non 2xx (e dei timeout > 5s) con backoff esponenziale. Sei tentativi totali in ~42 ore:
- Tentativo 1 — subito alla conferma.
- Tentativo 2 — +5 minuti.
- Tentativo 3 — +15 minuti.
- Tentativo 4 — +1 ora.
- Tentativo 5 — +4 ore.
- Tentativo 6 — +12 ore, poi +24 ore (finale).
Dopo 6 tentativi falliti l'evento va in dead-letter. Puoi ririchiedere lo stato attuale in qualsiasi momento via .
Problemi comuni
- Vedo 'invalid signature' a ogni delivery
- Il tuo framework ha parsato il body come JSON prima che tu lo hashassi. Leggi i byte RAW (Express: express.raw({type:'*/*'}); Next.js: req.text(); Laravel: request()->getContent(); Rails: request.raw_post). Non ri-serializzare mai prima dell'hash.
- IP whitelist — da quali IP mandate?
- Al momento le delivery partono da Vercel Edge (IP dinamici). Non pubblichiamo un range statico. Se DEVI whitelistare, usa l'header di firma come gate di auth e accetta qualsiasi IP sorgente — l'HMAC è il vero check di identità.
- HTTPS obbligatorio?
- Sì. Rifiutiamo di fare POST su endpoint http:// (rischio confusable deputy / replay in chiaro). L'URL https gratuito di ngrok funziona bene per il testing locale.
- Il mio endpoint è lento — posso estendere il timeout di 5s?
- No. Rispondi 2xx subito, poi processa asincrono (job queue, setImmediate, goroutine). Gli handler bloccanti lunghi finiscono sempre in timeout.
SDK
L'API è abbastanza piccola che va perfettamente bene — ma l'SDK Node ti dà i tipi, i retry automatici, e un helper che gestisce la verifica firma al posto tuo.
// 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-payTipi completi, helper webhook, retry automatici.
qualsiasi linguaggioUn POST, un GET. Nessuna libreria necessaria.
Commissioni
Flat — commissione totale Peptide-Pay. Niente abbonamento, niente canone mensile, niente commissioni di chargeback. Le commissioni on-ramp carta (~4.5% applicate dal card processor upstream) sono pass-through — le paga il cliente, non toccano mai il tuo payout.
| Metodo di pagamento | Paghi tu | Paga il cliente |
|---|---|---|
| Carta / Apple Pay / Google Pay | 3% | ~4.5% (on-ramp, pass-through) |
| Crypto diretto (USDC → USDC) | 3% | solo gas (~$0.01 su Polygon) |
Breakdown completo con esempi concreti su /fees.
Testing
Ogni nuovo account merchant riceve gratis — il 3% di commissione viene rimborsato sul tuo wallet entro 24h. Usali per provare il flusso end-to-end (carta vera, USDC veri, webhook veri) prima di andare live.
- Modalità sandbox è automatica: le prime 3 sessioni pagate per merchant sono marcate e hanno diritto al rimborso automatico.
- Dev card MoonPay: , qualsiasi scadenza futura, qualsiasi CVV, ZIP 10001.
- Testing webhook locale: esponi localhost con ngrok, incolla l'URL nel campo della sessione.
Loop locale completo (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.Errori e rate limit
Tutti gli errori hanno la forma . I codici di stato sono REST standard.
400401403404429502Troubleshooting
- La sessione è 'paid' in dashboard ma il mio webhook non è mai partito
- Verifica che webhook_url sia raggiungibile in HTTPS pubblico (curl da fuori la tua LAN). Se è corretto, fai polling su GET /sessions/{id} per confermare lo status — la dashboard /app mostra le stats di delivery webhook (success rate, contatori). Sei tentativi in 42h prima del dead-letter; puoi sempre risincronizzare via polling.
- Mismatch HMAC — la firma è sempre invalida
- Nel 99% dei casi: stai hashando un body ri-serializzato invece dei byte raw. I framework parsano JSON in automatico prima che parta il tuo handler; ti serve il buffer raw. Next.js: req.text() prima di qualsiasi .json(). Express: app.use('/webhooks', express.raw({ type: '*/*' }), …). Rails: request.raw_post. Controlla anche che calcoli `HMAC(whsec_secret, t + '.' + rawBody)` — NON solo `HMAC(whsec_secret, rawBody)`. Il prefisso timestamp è obbligatorio.
- MoonPay dice 'service unavailable in your country'
- MoonPay restringe ~20 paesi (Iran, Corea del Nord, Cuba, lista completa sul loro sito). Il provider di default è 'gateway' — lo smart picker fa auto-fallback a Revolut, Transak o Banxa che coprono geografie diverse. Se hai fissato uno specifico provider con provider: 'moonpay', rimuovilo e lascia scegliere il router.
- Il mio wallet non ha ricevuto USDC dopo un evento 'paid'
- Controlla polygonscan.com/address/<tuo-wallet> per i trasferimenti USDC (Polygon POS). Il settlement arriva al 97% a te e al 3% a Peptide-Pay - se non vedi il 97% in entrata, potresti aver incollato il wallet sbagliato nella chiamata init. Riconferma via GET /sessions/{id} - il campo txid punta al trasferimento on-chain reale.
- Il cliente è stato addebitato due volte
- Non dovrebbe succedere. Ogni sessione ha un solo addressIn di settlement; un secondo pagamento sullo stesso indirizzo diventa una sessione separata da parte nostra e accreditiamo solo il primo al tuo ordine. Se succede, screenshot dei due txid polygonscan + session id via email a hi@peptide-pay.com - rimborsiamo il duplicato dalla nostra treasury.
- Ricevo 502 'Payment infrastructure temporarily unavailable'
- Il nostro upstream di settlement è degradato (< 0.5% delle richieste). Retry in 30s con lo stesso Idempotency-Key - la nostra cache restituisce la risposta originale appena il wallet viene generato correttamente. Tieni d'occhio /status per incidenti live.
Pronto a integrare?
La maggior parte dei merchant va da zero alla prima transazione pagata in meno di 30 minuti.