Référence développeur

API Peptide-Pay

Une API REST pour encaisser cartes et crypto, avec settlement sur un wallet USDC que tu contrôles. Un POST crée un checkout. Un webhook te dit qu'il a payé. Live en moins de 30 minutes.

REST · JSONBearer authWebhooks HMAC-SHA256Idempotency-KeyCORS activé

Démarrer

Quickstart (5 min)

Trois étapes : créer une session, rediriger le client, gérer le webhook. L'exemple ci-dessous est une route de checkout Node.js prête pour la prod.

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

Voilà tout le happy path. Le client atterrit sur un checkout hébergé sur , choisit carte ou crypto, et ton webhook part dans les 30 s après paiement.

Authentification

Deux schémas, selon le mode d'intégration :

SchémaCommentQuand
Bearer tokenAuthorization: Bearer sk_live_…Côté serveur. Garde le wallet privé.
Wallet dans le body{ "wallet": "0x…", … }Sites statiques / widgets sans backend.
Attention
Les clés API portent l'identité complète du compte marchand — traite-les comme des mots de passe. Jamais de commit, jamais dans le navigateur, rotate-les depuis /app/api-keys en cas de fuite.
Astuce
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.

Référence API

URL de base : . Tous les endpoints parlent JSON, renvoient un seul objet en succès, et un objet sur les 4xx/5xx.

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

#Créer une session de checkout

Génère une URL de checkout hébergée. Le client l'ouvre, paie en carte ou en crypto, Peptide-Pay settle sur ton wallet en USDC, le webhook part.

Body de la requête
ChampTypeRequisDescription
amount_centsintegerrequisMontant dans la plus petite unité de la devise (centimes). Plage 100 – 10 000 000.
currencystringrequisCode ISO 4217. Supportés : EUR, USD, GBP, CAD, AUD, CHF.
walletstringone-ofWallet USDC sur Polygon (0x + 40 hex). Requis sauf si authentifié via Bearer key.
customer_emailstringoptionnelAffiché dans l'UI du checkout et transmis à l'on-ramp pour réutilisation KYC.
success_urlurloptionnelRedirection après paiement réussi. http/https uniquement.
cancel_urlurloptionnelRedirection si le client abandonne le checkout.
webhook_urlurloptionnelCible POST pour les événements order.paid. Écrase le défaut du dashboard.
providerstringoptionnelDéfaut 'gateway' (smart picker — recommandé). Ou pin un provider spécifique depuis GET /providers (ex. moonpay, revolut, banxa, transak).
product_namestringoptionnelLabel affiché sur la page de checkout (max 80 caractères).
metadataobjectoptionnelJusqu'à 10 paires clé/valeur string, renvoyées dans le webhook. Clé réservée : order_id.
Réponse (200 OK)
ChampTypeRequisDescription
idstringoptionnelSession id, commence par cs_.
urlstringoptionnelURL du checkout hébergé vers laquelle rediriger le client.
statusstringoptionnelToujours "pending" à la création.
amountintegeroptionnelEcho de amount_cents.
currencystringoptionnelEcho de currency.
providerstringoptionnelEcho de provider (défaut 'gateway').
expires_atstringoptionnelExpiration ISO 8601 (24h après création).
tracking_numberstringoptionnelAdresse de settlement Polygon — correspond à address_in dans le payload du webhook, utilisable avec /track pour le monitoring live.

Exemples

// 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);
Astuce
Passe pour sécuriser les retries. On rejoue la réponse cachée pendant 24h sur la même clé au lieu de générer une nouvelle session (évite le double-charge sur les réseaux instables).
GEThttps://peptide-pay.com/api/v1/sessions/{id}

#Récupérer une session (polling)

À utiliser comme fallback de webhook, ou pour hydrater une page de succès après redirection. Le serveur re-vérifie notre couche de settlement à chaque appel — pas cher (<200 ms), un poll toutes les 3-5 s est ok.

Réponse
ChampTypeRequisDescription
idstringoptionnelSession id.
statusstringoptionnelpending | paid | expired | failed.
amountintegeroptionnelMontant original en centimes.
currencystringoptionnelDevise originale.
paid_atstring|nulloptionnelISO 8601 quand le settlement on-chain s'est terminé.
paid_providerstring|nulloptionnelQuel provider a réellement traité le paiement (peut différer de celui demandé).
txidstring|nulloptionnelTxid de settlement Polygon. Lien vers polygonscan.com/tx/{txid}.
expires_atstringoptionnelExpiration 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

#Matrice provider live

Liste les on-ramps qui acceptent actuellement du trafic, avec les montants minimums par provider. Caché 5 min à l'edge — poll une fois au boot de l'app, pas par requête.

Réponse
ChampTypeRequisDescription
providers[].idstringoptionnelClé du provider (passable comme `provider` dans /checkout/init).
providers[].provider_namestringoptionnelLabel humain pour le dropdown.
providers[].statusstringoptionnel'active' (toujours filtré sur actif sur cet endpoint).
providers[].minimum_currencystringoptionnelCode ISO du minimum.
providers[].minimum_amountnumberoptionnelMontant le plus bas accepté par le provider (en unités 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

Quand une session atteint un état terminal, on POST un événement JSON signé sur le que tu as configuré (par session ou dans le dashboard). Parse toujours le body pour la vérification de signature — re-sérialiser le JSON réordonne les clés et casse le 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
}

Types d'événements

ÉvénementQuand
order.paidSettlement on-chain confirmé. + + garantis présents. Marque la commande payée.

Seul est livré aujourd'hui — les sessions expirées et failed sont observables via (le status passe à après le TTL de 24h ; les échecs terminaux affichent ). On pourra ajouter des push events pour celles-ci dans une prochaine release.

Vérification de signature

Les marchands avec un compte signup reçoivent un secret et chaque livraison embarque un header de la forme . Calcule et compare en constant-time à . Rejette tout ce qui a plus de 5 minutes.

Astuce
Les flows wallet-only (pas de signup, pas de ) livrent le webhook non signé — tu dois quand même valider que correspond à une session que tu as créée. Pour des livraisons signées, inscris-toi sur /signup pour récupérer ton 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');
}
Attention
Utilise toujours la comparaison constant-time de ton langage : (Node), (Python), (PHP), (Ruby). Un simple leak le HMAC un byte à la fois à un attaquant timing.

Politique de retry

On retry les réponses non-2xx (et les timeouts > 5 s) avec un backoff exponentiel. Six tentatives au total sur ~42 heures :

  • Tentative 1 — immédiatement à la confirmation.
  • Tentative 2 — +5 minutes.
  • Tentative 3 — +15 minutes.
  • Tentative 4 — +1 heure.
  • Tentative 5 — +4 heures.
  • Tentative 6 — +12 heures, puis +24 heures (finale).

Après 6 tentatives échouées, l'événement est dead-lettered. Tu peux re-demander l'état courant à tout moment via .

Astuce
Rends ton handler idempotent. Dédupe sur — le retry peut re-tirer un événement paid que tu as déjà traité.

Problèmes fréquents

Je vois 'invalid signature' à chaque livraison
Ton framework a parsé le body en JSON avant que tu le hashes. Lis les bytes BRUTS (Express : express.raw({type:'*/*'}) ; Next.js : req.text() ; Laravel : request()->getContent() ; Rails : request.raw_post). Ne re-sérialise jamais avant de hasher.
Whitelist d'IP — tu envoies depuis quelles IPs ?
Les livraisons partent actuellement de Vercel Edge (IPs dynamiques). On ne publie pas de range statique. Si tu DOIS whitelister, utilise le header de signature comme porte d'entrée auth et accepte n'importe quelle IP source — le HMAC est le vrai check d'identité.
HTTPS requis ?
Oui. On refuse de POST vers des endpoints http:// (risque de confused deputy / replay plaintext). L'URL https gratuite de ngrok marche très bien pour les tests locaux.
Mon endpoint est lent — je peux étendre le timeout 5 s ?
Non. Réponds 2xx immédiatement, puis traite en asynchrone (queue de jobs, setImmediate, goroutine). Les handlers bloquants finissent toujours en timeout.

SDKs

L'API est assez petite pour que suffise largement — mais le SDK Node te donne des types, des retries automatiques et un helper qui gère la vérification de signature pour toi.

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

Types complets, helper webhook, retries automatiques.

Direct fetch()marche toujours
tous langages

Un POST, un GET. Pas besoin de librairie.

Astuce
Les SDKs Python, PHP, Ruby et Go sont sur la roadmap. Jusqu'à leur release, les exemples // bruts ci-dessus sont la référence canonique — on ne les cassera pas.

Tarifs

flat — la commission totale de Peptide-Pay. Pas d'abonnement, pas de frais mensuels, pas de frais de chargeback. Les frais d'on-ramp carte (~4,5% prélevés par le processeur carte upstream) sont en pass-through — le client les paie, ils ne touchent jamais ton payout.

Moyen de paiementTu paiesLe client paie
Carte / Apple Pay / Google Pay3%~4,5% (on-ramp, pass-through)
Crypto direct (USDC → USDC)3%gas seulement (~0,01 $ sur Polygon)

Breakdown complet avec exemples chiffrés sur /fees.

Tests

Chaque nouveau compte marchand reçoit gratuites — la commission de 3% est remboursée sur ton wallet sous 24h. Utilise-les pour répéter tout le flow de bout en bout (vraie carte, vrai USDC, vrai webhook) avant de passer en live.

  • Le mode sandbox est automatique : les 3 premières sessions payées par marchand sont marquées et qualifient pour le remboursement auto.
  • Carte de dev MoonPay : , n'importe quelle date future, n'importe quel CVV, ZIP 10001.
  • Test webhook local : expose localhost avec ngrok, colle l'URL dans le champ par session.

Boucle locale complète (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.

Erreurs & rate limits

Toutes les erreurs partagent la forme . Codes de statut REST standards.

400
JSON invalide ou champ manquant
Body mal formé, amount non numérique, wallet pas une adresse 0x, devise non supportée.
401
Clé API invalide ou révoquée
Le Bearer token ne résout pas vers un marchand. Rotate-la sur /app/api-keys.
403
Signature de callback invalide
Interne — notre IPN de settlement a tapé le récepteur webhook sans la bonne signature par session. Pas une erreur visible côté marchand en opération normale.
404
Session introuvable
Mauvais id, ou session purgée (> 90j après état terminal).
429
Rate limit dépassé
60 req/min/IP sur init, 30 req/min/IP sur select. Header Retry-After inclus. Contacte le support pour des tiers plus élevés.
502
Upstream indisponible
Réseau de settlement temporairement dégradé. Retry dans 30 s avec la même Idempotency-Key. Cible SLA : 99,5%+.

Troubleshooting

La session est 'paid' dans le dashboard mais mon webhook n'a jamais tiré
Vérifie que webhook_url est joignable en HTTPS public (curl-le depuis l'extérieur de ton LAN). Si c'est bon, poll GET /sessions/{id} pour confirmer le status — le dashboard /app affiche les stats de livraison webhook (taux de succès, compteurs). Six tentatives sur 42h avant dead-letter ; tu peux toujours resynchroniser en polling.
Mismatch HMAC — la signature est toujours invalide
Dans 99% des cas : tu hashes un body re-sérialisé au lieu des bytes bruts. Les frameworks parsent le JSON automatiquement avant que ton handler ne tourne ; il te faut le buffer brut. Next.js : req.text() avant tout .json(). Express : app.use('/webhooks', express.raw({ type: '*/*' }), …). Rails : request.raw_post. Vérifie aussi que tu calcules `HMAC(whsec_secret, t + '.' + rawBody)` — PAS juste `HMAC(whsec_secret, rawBody)`. Le préfixe timestamp est requis.
MoonPay dit 'service unavailable in your country'
MoonPay restreint ~20 pays (Iran, Corée du Nord, Cuba, liste complète sur leur site). Le provider par défaut est 'gateway' — le smart picker bascule automatiquement sur Revolut, Transak ou Banxa qui couvrent des géographies différentes. Si tu as pinné un provider spécifique avec provider: 'moonpay', enlève-le et laisse le router choisir.
Mon wallet n'a pas reçu d'USDC après un événement 'paid'
Vérifie polygonscan.com/address/<your-wallet> pour les transferts USDC (Polygon POS). Le settlement atterrit à 97% chez toi et 3% chez Peptide-Pay — si tu ne vois pas les 97% en entrée, tu as peut-être collé le mauvais wallet dans l'appel init. Re-confirme via GET /sessions/{id} — le champ txid pointe vers le vrai transfert on-chain.
Le client a été débité deux fois
Ne devrait pas arriver. Chaque session a un seul addressIn de settlement ; un second paiement sur la même adresse devient une session séparée de notre côté, et on ne crédite que le premier sur ta commande. Si ça arrive, screenshot les deux txids polygonscan + le session id et envoie à hi@peptide-pay.com — on rembourse le doublon depuis notre trésorerie.
Je reçois un 502 'Payment infrastructure temporarily unavailable'
Notre upstream settlement est dégradé (< 0,5% des requêtes). Retry dans 30 s avec la même Idempotency-Key — notre cache renvoie la réponse originale dès que le wallet est généré avec succès. Surveille /status pour les incidents live.

Prêt à intégrer ?

La plupart des marchands passent de zéro à la première transaction payée en moins de 30 minutes.