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.
Démarrer
Peptide-Pay propose 4 stacks d'intégration (Shopify, WooCommerce, builders IA, API directe). Cette page documente le mode authentification — c'est le même pour les 4. Vue d'ensemble visuelle des stacks : voir /integrate.
Bouton drop-in, pas de signup. Passe ton adresse wallet USDC dans le body. Zéro state backend ; public, visible dans les DevTools.
Server-to-server. Cache ton wallet derrière un Bearer sk_live_. Configure le branding, les webhooks et les mass payouts depuis le dashboard.
Plugin prêt à l'emploi. Upload le ZIP, colle ta clé API + webhook secret, les commandes se complètent auto au paiement. HPOS-ready, WC 7.0+.
Custom App + Méthode de paiement manuelle. ~30 min sur une boutique existante. On marque les commandes payées via l'Admin API Shopify.
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.
// 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éma | Comment | Quand |
|---|---|---|
| Bearer token | Authorization: Bearer sk_live_… | Côté serveur. Garde le wallet privé. |
| Wallet dans le body | { "wallet": "0x…", … } | Sites statiques / widgets sans 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.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.
#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.
| Champ | Type | Requis | Description |
|---|---|---|---|
| amount_cents | integer | requis | Montant dans la plus petite unité de la devise (centimes). Plage 100 – 10 000 000. |
| currency | string | requis | Code ISO 4217. Supportés : EUR, USD, GBP, CAD, AUD, CHF. |
| wallet | string | one-of | Wallet USDC sur Polygon (0x + 40 hex). Requis sauf si authentifié via Bearer key. |
| customer_email | string | optionnel | Affiché dans l'UI du checkout et transmis à l'on-ramp pour réutilisation KYC. |
| success_url | url | optionnel | Redirection après paiement réussi. http/https uniquement. |
| cancel_url | url | optionnel | Redirection si le client abandonne le checkout. |
| webhook_url | url | optionnel | Cible POST pour les événements order.paid. Écrase le défaut du dashboard. |
| provider | string | optionnel | Défaut 'gateway' (smart picker — recommandé). Ou pin un provider spécifique depuis GET /providers (ex. moonpay, revolut, banxa, transak). |
| product_name | string | optionnel | Label affiché sur la page de checkout (max 80 caractères). |
| metadata | object | optionnel | Jusqu'à 10 paires clé/valeur string, renvoyées dans le webhook. Clé réservée : order_id. |
| Champ | Type | Requis | Description |
|---|---|---|---|
| id | string | optionnel | Session id, commence par cs_. |
| url | string | optionnel | URL du checkout hébergé vers laquelle rediriger le client. |
| status | string | optionnel | Toujours "pending" à la création. |
| amount | integer | optionnel | Echo de amount_cents. |
| currency | string | optionnel | Echo de currency. |
| provider | string | optionnel | Echo de provider (défaut 'gateway'). |
| expires_at | string | optionnel | Expiration ISO 8601 (24h après création). |
| tracking_number | string | optionnel | Adresse 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);#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.
| Champ | Type | Requis | Description |
|---|---|---|---|
| id | string | optionnel | Session id. |
| status | string | optionnel | pending | paid | expired | failed. |
| amount | integer | optionnel | Montant original en centimes. |
| currency | string | optionnel | Devise originale. |
| paid_at | string|null | optionnel | ISO 8601 quand le settlement on-chain s'est terminé. |
| paid_provider | string|null | optionnel | Quel provider a réellement traité le paiement (peut différer de celui demandé). |
| txid | string|null | optionnel | Txid de settlement Polygon. Lien vers polygonscan.com/tx/{txid}. |
| expires_at | string | optionnel | Expiration 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
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.
| Champ | Type | Requis | Description |
|---|---|---|---|
| providers[].id | string | optionnel | Clé du provider (passable comme `provider` dans /checkout/init). |
| providers[].provider_name | string | optionnel | Label humain pour le dropdown. |
| providers[].status | string | optionnel | 'active' (toujours filtré sur actif sur cet endpoint). |
| providers[].minimum_currency | string | optionnel | Code ISO du minimum. |
| providers[].minimum_amount | number | optionnel | Montant 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énement | Quand |
|---|---|
| order.paid | Settlement 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.
// 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');
}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 .
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
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-payTypes complets, helper webhook, retries automatiques.
tous langagesUn POST, un GET. Pas besoin de librairie.
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 paiement | Tu paies | Le client paie |
|---|---|---|
| Carte / Apple Pay / Google Pay | 3% | ~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.
400401403404429502Troubleshooting
- 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.