Referencia para developers

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.

REST · JSONBearer authWebhooks HMAC-SHA256Idempotency-KeyCORS activado

Primeros pasos

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.

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);

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:

EsquemaCómoCuándo
Bearer tokenAuthorization: Bearer sk_live_…Server-side. Mantiene la wallet privada.
Wallet en el body{ "wallet": "0x…", … }Sitios estáticos / widgets sin backend.
Atención
Las API keys llevan la identidad completa de la cuenta merchant — trátalas como contraseñas. Nunca las commits, nunca las envíes al navegador, rótalas desde /app/api-keys si se filtran.
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.

Referencia de la API

Base URL: . Todos los endpoints hablan JSON, devuelven un objeto en éxito y un en 4xx/5xx.

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

#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.

Request body
CampoTipoObligatorioDescripción
amount_centsintegerobligatorioImporte en la unidad más pequeña de la moneda (céntimos). Rango 100 – 10 000 000.
currencystringobligatorioCódigo ISO 4217. Soportados: EUR, USD, GBP, CAD, AUD, CHF.
walletstringone-ofWallet USDC en Polygon (0x + 40 hex). Obligatorio salvo que autentiques con Bearer key.
customer_emailstringopcionalSe muestra en la UI del checkout y se reenvía al on-ramp para reutilizar KYC.
success_urlurlopcionalRedirect tras pago exitoso. Solo http/https.
cancel_urlurlopcionalRedirect si el cliente abandona el checkout.
webhook_urlurlopcionalDestino del POST para eventos order.paid. Sobrescribe el default del dashboard.
providerstringopcionalPor defecto 'gateway' (smart picker — recomendado). O fija un id de on-ramp específico desde GET /providers (ej. moonpay, revolut, banxa, transak).
product_namestringopcionalEtiqueta mostrada en la página de checkout (máx 80 chars).
metadataobjectopcionalHasta 10 pares string key/value, devueltos tal cual en el webhook. Clave reservada: order_id.
Response (200 OK)
CampoTipoObligatorioDescripción
idstringopcionalId de sesión, empieza por cs_.
urlstringopcionalURL del checkout hospedado para redirigir al cliente.
statusstringopcionalSiempre "pending" al crearse.
amountintegeropcionalEcho de amount_cents.
currencystringopcionalEcho de currency.
providerstringopcionalEcho de provider (por defecto 'gateway').
expires_atstringopcionalExpiración ISO 8601 (24h desde la creación).
tracking_numberstringopcionalDirecció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);
Tip
Pasa para que los reintentos sean seguros. Reproducimos la respuesta cacheada durante 24h con la misma key en vez de crear una nueva sesión (previene doble cobro en redes inestables).
GEThttps://peptide-pay.com/api/v1/sessions/{id}

#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.

Response
CampoTipoObligatorioDescripción
idstringopcionalId de sesión.
statusstringopcionalpending | paid | expired | failed.
amountintegeropcionalImporte original en céntimos.
currencystringopcionalMoneda original.
paid_atstring|nullopcionalISO 8601 del momento en que se completó el settlement on-chain.
paid_providerstring|nullopcionalQué proveedor procesó el pago realmente (puede diferir del solicitado).
txidstring|nullopcionalTxid del settlement en Polygon. Link con polygonscan.com/tx/{txid}.
expires_atstringopcionalExpiració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');
}
GEThttps://peptide-pay.com/api/v1/providers

#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.

Response
CampoTipoObligatorioDescripción
providers[].idstringopcionalKey del proveedor (utilizable como `provider` en /checkout/init).
providers[].provider_namestringopcionalEtiqueta legible para el dropdown.
providers[].statusstringopcional'active' (siempre filtrado a activos en este endpoint).
providers[].minimum_currencystringopcionalCódigo ISO del mínimo.
providers[].minimum_amountnumberopcionalMenor 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

EventoCuándo
order.paidSettlement 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.

Tip
Los flujos wallet-only (sin signup, sin ) entregan el webhook sin firma — aun así deberías validar que corresponde a una sesión que tú creaste. Para entregas firmadas, regístrate en /signup para obtener tu 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');
}
Atención
Usa siempre el compare en tiempo constante de tu lenguaje: (Node), (Python), (PHP), (Ruby). Un normal filtra el HMAC byte a byte a un timing attacker.

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 .

Tip
Haz tu handler idempotente. Deduplica por — el retry puede re-disparar un evento paid que ya procesaste.

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
// 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

Tipos completos, helper de webhook, retries automáticos.

fetch() directosiempre funciona
cualquier lenguaje

Un POST, un GET. Sin librería.

Tip
SDKs de Python, PHP, Ruby y Go están en roadmap. Hasta que salgan, los ejemplos crudos de // de arriba son la referencia canónica — no los romperemos.

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 pagoTú pagasEl cliente paga
Tarjeta / Apple Pay / Google Pay3%~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.

400
JSON inválido o campo ausente
Body malformado, amount no numérico, wallet no es una dirección 0x, currency no soportada.
401
API key inválida o revocada
El Bearer token no resuelve a ningún merchant. Rótalo en /app/api-keys.
403
Firma de callback mala
Interno — nuestro IPN de settlement llegó al receiver del webhook sin la firma por sesión correcta. No es un error visible al merchant en operación normal.
404
Sesión no encontrada
Id incorrecto, o la sesión fue purgada (> 90d tras estado terminal).
429
Rate limit excedido
60 req/min/IP en init, 30 req/min/IP en select. Header Retry-After incluido. Contacta soporte para tiers mayores.
502
Upstream no disponible
Red de settlement temporalmente degradada. Reintenta en 30s con la misma Idempotency-Key. Target SLA: 99.5%+.

Troubleshooting

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.