API de Peptide-Pay
Una API REST para cobrar tarjetas y cripto, con settlement a una wallet USDC que tú controlas. Un POST crea un checkout. Un webhook te avisa de que pagó. En marcha en menos de 30 minutos.
Primeros pasos
Peptide-Pay soporta cuatro modos de integración. Elige el que encaje con tus restricciones; la API por debajo es la misma.
Botón drop-in, sin signup. Pasas tu wallet USDC en el body de la request. Zero estado en backend; público, visible en DevTools.
Servidor a servidor. Oculta tu wallet detrás de un Bearer sk_live_. Configura branding, webhooks y mass payouts desde el dashboard.
Plugin listo para usar. Subes el ZIP, pegas tu API key + webhook secret, los pedidos se auto-completan al pagar. HPOS-ready, WC 7.0+.
Custom App + Método de Pago Manual. ~30 min de instalación en una tienda existente. Marcamos los pedidos como pagados vía Shopify Admin API.
Quickstart (5 min)
Tres pasos: crear una sesión, redirigir al cliente, manejar el webhook. El ejemplo de abajo es una ruta de checkout Node.js lista para producción.
// 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);Ese es todo el happy path. El cliente aterriza en un checkout hospedado en , elige tarjeta o cripto, y tu webhook se dispara en menos de 30s tras el pago.
Autenticación
Dos esquemas, según el modo de integración:
| Esquema | Cómo | Cuándo |
|---|---|---|
| Bearer token | Authorization: Bearer sk_live_… | Server-side. Mantiene la wallet privada. |
| Wallet en el body | { "wallet": "0x…", … } | Sitios estáticos / widgets sin 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.Referencia de la API
Base URL: . Todos los endpoints hablan JSON, devuelven un objeto en éxito y un en 4xx/5xx.
#Crear una sesión de checkout
Genera una URL de checkout hospedado. El cliente la abre, paga con tarjeta o cripto, Peptide-Pay hace settlement a tu wallet en USDC y se dispara el webhook.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| amount_cents | integer | obligatorio | Importe en la unidad más pequeña de la moneda (céntimos). Rango 100 – 10 000 000. |
| currency | string | obligatorio | Código ISO 4217. Soportados: EUR, USD, GBP, CAD, AUD, CHF. |
| wallet | string | one-of | Wallet USDC en Polygon (0x + 40 hex). Obligatorio salvo que autentiques con Bearer key. |
| customer_email | string | opcional | Se muestra en la UI del checkout y se reenvía al on-ramp para reutilizar KYC. |
| success_url | url | opcional | Redirect tras pago exitoso. Solo http/https. |
| cancel_url | url | opcional | Redirect si el cliente abandona el checkout. |
| webhook_url | url | opcional | Destino del POST para eventos order.paid. Sobrescribe el default del dashboard. |
| provider | string | opcional | Por defecto 'gateway' (smart picker — recomendado). O fija un id de on-ramp específico desde GET /providers (ej. moonpay, revolut, banxa, transak). |
| product_name | string | opcional | Etiqueta mostrada en la página de checkout (máx 80 chars). |
| metadata | object | opcional | Hasta 10 pares string key/value, devueltos tal cual en el webhook. Clave reservada: order_id. |
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| id | string | opcional | Id de sesión, empieza por cs_. |
| url | string | opcional | URL del checkout hospedado para redirigir al cliente. |
| status | string | opcional | Siempre "pending" al crearse. |
| amount | integer | opcional | Echo de amount_cents. |
| currency | string | opcional | Echo de currency. |
| provider | string | opcional | Echo de provider (por defecto 'gateway'). |
| expires_at | string | opcional | Expiración ISO 8601 (24h desde la creación). |
| tracking_number | string | opcional | Dirección de settlement en Polygon — coincide con address_in en el payload del webhook, usable con /track para monitoreo en vivo. |
Ejemplos
// 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);#Recuperar una sesión (polling)
Úsalo como fallback del webhook, o para hidratar una success page tras el redirect. Server-side re-chequea nuestra capa de settlement en cada llamada — barato (<200ms), así que hacer polling cada 3-5s está bien.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| id | string | opcional | Id de sesión. |
| status | string | opcional | pending | paid | expired | failed. |
| amount | integer | opcional | Importe original en céntimos. |
| currency | string | opcional | Moneda original. |
| paid_at | string|null | opcional | ISO 8601 del momento en que se completó el settlement on-chain. |
| paid_provider | string|null | opcional | Qué proveedor procesó el pago realmente (puede diferir del solicitado). |
| txid | string|null | opcional | Txid del settlement en Polygon. Link con polygonscan.com/tx/{txid}. |
| expires_at | string | opcional | Expiración 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');
}#Matriz de proveedores en vivo
Lista los on-ramps aceptando tráfico ahora mismo, con importes mínimos por proveedor. Cacheado 5 min en el edge — haz polling una vez al arrancar la app, no por request.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
| providers[].id | string | opcional | Key del proveedor (utilizable como `provider` en /checkout/init). |
| providers[].provider_name | string | opcional | Etiqueta legible para el dropdown. |
| providers[].status | string | opcional | 'active' (siempre filtrado a activos en este endpoint). |
| providers[].minimum_currency | string | opcional | Código ISO del mínimo. |
| providers[].minimum_amount | number | opcional | Menor importe que acepta el proveedor (en unidades de 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.Webhooks
Cuando una sesión llega a un estado terminal hacemos POST de un evento JSON firmado a la que configuraste (por sesión o en el dashboard). Parsea siempre el body de la request para verificar la firma — re-serializar JSON reordena las claves y rompe el 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
}Tipos de evento
| Evento | Cuándo |
|---|---|
| order.paid | Settlement on-chain confirmado. + + garantizados. Marca el pedido como pagado. |
Hoy solo se entrega — las sesiones expired y failed son observables vía (status pasa a tras el TTL de 24h; los fallos terminales muestran ). Puede que añadamos push events para ellos en una release futura.
Verificación de firma
Los merchants con cuenta de signup reciben un secret y cada entrega lleva un header con la forma . Computa y compara en tiempo constante contra . Rechaza cualquier cosa más antigua de 5 minutos.
// 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');
}Política de reintentos
Reintentamos respuestas no-2xx (y timeouts > 5s) con backoff exponencial. Seis intentos en total a lo largo de ~42 horas:
- Intento 1 — inmediatamente al confirmarse.
- Intento 2 — +5 minutos.
- Intento 3 — +15 minutos.
- Intento 4 — +1 hora.
- Intento 5 — +4 horas.
- Intento 6 — +12 horas, luego +24 horas (final).
Tras 6 intentos fallidos el evento pasa a dead-letter. Puedes volver a pedir el estado actual en cualquier momento vía .
Problemas comunes
- Veo 'invalid signature' en todas las entregas
- Tu framework parseó el body como JSON antes de que lo hashearas. Lee los bytes RAW (Express: express.raw({type:'*/*'}); Next.js: req.text(); Laravel: request()->getContent(); Rails: request.raw_post). Nunca re-serialices antes de hashear.
- IP whitelist — ¿desde qué IPs enviáis?
- Las entregas salen actualmente de Vercel Edge (IPs dinámicas). No publicamos un rango estático. Si TIENES que hacer whitelist, usa el header de firma como tu auth gate y acepta cualquier IP origen — el HMAC es la verdadera prueba de identidad.
- ¿HTTPS obligatorio?
- Sí. Nos negamos a hacer POST a endpoints http:// (riesgo de confused deputy / replay en plaintext). La URL https gratuita de ngrok funciona para testing local.
- Mi endpoint es lento — ¿puedo extender el timeout de 5s?
- No. Responde 2xx inmediatamente, luego procesa asíncronamente (cola de jobs, setImmediate, goroutine). Los handlers que bloquean mucho terminan siempre en timeout.
SDKs
La API es lo bastante pequeña como para que esté perfectamente bien — pero el SDK de Node te da tipos, retries automáticos y un helper que te gestiona la verificación de firma.
// 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-payTipos completos, helper de webhook, retries automáticos.
cualquier lenguajeUn POST, un GET. Sin librería.
Tarifas
Flat — la comisión completa de Peptide-Pay. Sin suscripción, sin cuota mensual, sin fees de chargeback. Las comisiones del on-ramp de tarjeta (~4.5% cobrados por el procesador upstream) son pass-through — las paga el cliente, nunca tocan tu payout.
| Método de pago | Tú pagas | El cliente paga |
|---|---|---|
| Tarjeta / Apple Pay / Google Pay | 3% | ~4.5% (on-ramp, pass-through) |
| Cripto directo (USDC → USDC) | 3% | solo gas (~$0.01 en Polygon) |
Desglose completo con ejemplos en /fees.
Testing
Cada cuenta nueva de merchant recibe gratis — el 3% completo se reembolsa a tu wallet en 24h. Úsalos para ensayar el flujo entero end-to-end (tarjeta real, USDC real, webhook real) antes de ir a producción.
- Modo sandbox es automático: las 3 primeras sesiones pagadas por merchant se marcan y califican para auto-refund.
- Tarjeta de dev de MoonPay: , cualquier expiración futura, cualquier CVV, ZIP 10001.
- Testing local de webhook: expón localhost con ngrok, pega la URL en el campo por sesión.
Loop local 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.Errores y rate limits
Todos los errores comparten la forma . Los status codes son REST estándar.
400401403404429502Troubleshooting
- La sesión está 'paid' en el dashboard pero mi webhook nunca se disparó
- Comprueba que webhook_url es accesible por HTTPS pública (cúrlalo desde fuera de tu LAN). Si es correcta, haz polling GET /sessions/{id} para confirmar el status — el dashboard /app muestra stats de entrega (tasa de éxito, conteos). Seis intentos en 42h antes del dead-letter; siempre puedes re-sincronizar por polling.
- HMAC mismatch — la firma siempre es inválida
- El 99% de las veces: estás hasheando un body re-serializado en vez de los bytes raw. Los frameworks auto-parsean JSON antes de tu handler; necesitas el buffer raw. Next.js: req.text() antes de cualquier .json(). Express: app.use('/webhooks', express.raw({ type: '*/*' }), …). Rails: request.raw_post. Verifica también que computas `HMAC(whsec_secret, t + '.' + rawBody)` — NO solo `HMAC(whsec_secret, rawBody)`. El prefijo del timestamp es obligatorio.
- MoonPay dice 'service unavailable in your country'
- MoonPay restringe ~20 países (Irán, Corea del Norte, Cuba, lista completa en su web). El provider por defecto es 'gateway' — el smart picker hace auto-fallback a Revolut, Transak o Banxa, que cubren geografías distintas. Si fijaste un provider específico con provider: 'moonpay', quítalo y deja que el router elija.
- Mi wallet no recibió USDC tras un evento 'paid'
- Revisa polygonscan.com/address/<tu-wallet> por transferencias de USDC (Polygon POS). El settlement llega 97% a ti y 3% a Peptide-Pay - si no ves el 97% entrando, puede que hayas pegado la wallet equivocada en la llamada a init. Re-confirma con GET /sessions/{id} - el campo txid apunta a la transferencia on-chain real.
- El cliente fue cobrado dos veces
- No debería pasar. Cada sesión tiene un único addressIn de settlement; un segundo pago a la misma dirección se convierte en una sesión separada en nuestro lado y solo acreditamos el primero a tu pedido. Si pasa, captura los dos txids de polygonscan + el id de sesión y escribe a hi@peptide-pay.com - devolvemos el duplicado desde nuestra treasury.
- Me sale 502 'Payment infrastructure temporarily unavailable'
- Nuestro upstream de settlement está degradado (< 0.5% de las requests). Reintenta en 30s con la misma Idempotency-Key - nuestro cache devuelve la respuesta original en cuanto la wallet mintea. Mira /status para incidentes en vivo.
¿Listo para integrar?
La mayoría de merchants van de cero a su primera transacción pagada en menos de 30 minutos.