Acceptez des paiements
en quelques lignes de code
Créez une session de paiement depuis votre serveur, redirigez votre client vers la page de paiement hébergée, recevez un webhook signé à l’encaissement. Facture générée automatiquement, comptabilité tenue à jour.
Choisir son intégration
Trois niveaux, du plus simple au plus complet. Choisissez selon ce que vous vendez et vos moyens techniques — vous pourrez toujours monter en puissance plus tard.
Prix fixe du catalogue (formation, abonnement). Six lignes de HTML, clé pk_, aucun backend. → SDK, mode 1
Montant variable (panier e-commerce). Le snippet nexus.js appelle votre serveur, qui fixe le prix avec sa clé sk_. → SDK, mode 2
ERP, SaaS, CRM. Facturation, devis et réconciliation automatisés, webhooks signés, mode test étanche, SDK Node/PHP.
Intégrer Nexus à un ERP — parcours test → live
Le chemin recommandé pour une intégration API complète, sans jamais toucher aux vraies données avant d’être prêt :
- 1. Deux clés. Créez une clé
sk_test_(développement) et unesk_live_(coffre, pour plus tard). Restreignez leurs scopes au strict besoin. Côté ERP :NEXUS_API_KEY=sk_test_…. - 2. Le webhook d’abord. Créez un endpoint, stockez son
signingSecret, branchezverifyWebhookSignature(corps brut, réponse 2xx rapide, déduplication parX-Nexus-Delivery). Validez avecPOST /webhook_endpoints/{id}/test. - 3. Développez en mode test. Tout est étanche : numéros
TEST-*, zéro comptabilité, aucun argent réel. Construisez vos flux (customers,invoicescreate/send/mark_paid/credit_note,quotes), réglez les sessions de test parsimulate_payment. - 4. Recette. Cas d’erreur (
403/409/422/429), pagination (has_more/starting_after), résilience webhook (répondre 500 une fois → le retry arrive ~1 min après, visible danswebhook_deliveries). - 5. Bascule en live. Créez l’endpoint webhook de prod, remplacez la variable d’environnement par
sk_live_— zéro changement de code. Smoke test sur une vraie facture, et c’est en production.
simulate_payment (qui déclenche le vrai webhook signé), pas par les cartes 4242…. Pour un ERP dont le cœur est la facturation et le virement, cela couvre tout le périmètre.Démarrage en 5 minutes
Obtenez une clé API
Depuis le dashboard marchand, ouvrez Développeurs & Webhooks et créez une clé secrète (sk_…). Elle n’est affichée qu’une seule fois — stockez-la dans un gestionnaire de secrets, jamais dans votre code client.
Créez une session de paiement (côté serveur)
curl -X POST https://api.nexuspay.fr/api/v1/checkout/sessions \
-H "Authorization: Bearer sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: commande-1042" \
-d '{
"amount": 4900,
"currency": "EUR",
"description": "Abonnement Pro — juin",
"customer_email": "client@exemple.fr",
"success_url": "https://votre-site.fr/merci?commande=1042",
"cancel_url": "https://votre-site.fr/panier"
}'Les montants sont toujours exprimés en centimes (4900 = 49,00 €). La réponse contient une url de paiement hébergée, valable 24 heures.
Redirigez votre client
const res = await fetch('https://api.nexuspay.fr/api/v1/checkout/sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.NEXUS_SECRET_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': 'commande-1042',
},
body: JSON.stringify({
amount: 4900,
currency: 'EUR',
description: 'Abonnement Pro — juin',
customer_email: 'client@exemple.fr',
success_url: 'https://votre-site.fr/merci?commande=1042',
}),
});
const session = await res.json();
// session.id → "ps_00000042"
// session.url → "https://nexuspay.fr/checkout/<token>"
// puis redirigez votre client vers session.url (HTTP 303)Confirmez l’encaissement par webhook
Configurez un endpoint webhook dans le dashboard. À l’encaissement, Nexus envoie l’événement payment.session.succeeded signé HMAC. C’est la source de vérité pour livrer la commande — la redirection navigateur vers success_url ne suffit pas (voir Webhooks).
Authentification & clés API
Toutes les requêtes vers /api/v1/ s’authentifient par en-tête Authorization: Bearer <clé>. Deux types de clés, au format pk_live_… / sk_live_… (préfixe + 48 caractères hexadécimaux) :
product_id, sku ou line_items) : le prix est fixé côté serveur Nexus, un visiteur ne peut pas le modifier. Le montant libre est refusé (403).Scopes
À la création d’une clé, le paramètre optionnel scopes restreint ses droits à une liste d’endpoints. Une clé sans scopes a accès complet (comportement par défaut, rétrocompatible avec les clés existantes) ; une liste vide est refusée (400). Quatorze scopes existent, chacun couvrant des endpoints précis :
| Scope | Endpoints autorisés |
|---|---|
checkout:write | POST /api/v1/checkout/sessions |
checkout:read | GET /api/v1/checkout/sessions/{id} |
refunds:write | POST /api/v1/checkout/sessions/{id}/refund |
products:read | GET /api/v1/products[/{id}] |
products:write | POST | PATCH | DELETE /api/v1/products[/{id}] |
customers:read | GET /api/v1/customers[/{id}] |
customers:write | POST | PATCH | DELETE /api/v1/customers[/{id}] |
invoices:read | GET /api/v1/invoices[/{id}[/pdf]] |
invoices:write | POST /api/v1/invoices[/{id}/finalize | send] |
webhooks:read | GET /api/v1/webhook_endpoints | webhook_deliveries |
webhooks:write | POST | PATCH | DELETE /api/v1/webhook_endpoints[/{id}] · replay |
quotes:read | GET /api/v1/quotes[/{id}[/pdf]] |
quotes:write | POST /api/v1/quotes[/{id}/send | convert] |
payments:read | GET /api/v1/payments[/{id}] |
Une requête vers un endpoint non couvert par les scopes de la clé reçoit 403 avec le scope manquant :
{ "error": "insufficient_scope", "required": "refunds:write" }Mode test
Créez une clé de test en passant { "mode": "test" } à la création (clé préfixée sk_test_ / pk_test_). Les ressources créées par une clé test portent livemode: false et vivent dans un bac à sable étanche :
| Comportement | Mode test |
|---|---|
| Clients & factures | Visibles uniquement par les clés test — jamais dans le dashboard ni via une clé live (et inversement). |
| Numérotation | Préfixe TEST- sur une séquence à part : la numérotation légale ne consomme aucun numéro. |
| Comptabilité & fiscal | Aucune écriture comptable, exclues de la CA3/DEB/DES, des clôtures et des agrégats. |
| Paiement | Pas de page de paiement (paymentUrl: null) — aucun argent réel ne peut transiter. |
| Webhooks | Émis normalement avec data.livemode: false. Testez la signature avec POST /api/v1/webhook_endpoints/{id}/test. |
Checkout en mode test : une session créée par une clé test porte livemode: false et ne peut jamais encaisser d’argent réel (le paiement carte y est bloqué). Réglez-la côté serveur avec POST /api/v1/checkout/sessions/{id}/simulate_payment (clé test uniquement) : la session passe en succeeded et le webhook payment.session.succeeded part normalement (livemode: false, simulated: true) — votre intégration se teste de bout en bout, signature comprise. Pas de facture auto ni d’écriture comptable en test. (Le paiement par carte de test — cartes 4242… — viendra avec l’environnement Stripe de test.)
Sessions de paiement
/api/v1/checkout/sessionssk_ ou pk_Crée une session de paiement et retourne son URL de page hébergée. Les sessions expirent au bout de 24 heures. Statuts possibles : pending → succeeded | failed | expired | refunded.
Corps de la requête
amountintegercurrencystringdescriptionstringcustomer_emailstringcustomer_namestringcustomer_addressobjectcustomer_siretstringcustomer_vat_numberstringcustomer_external_idstringsuccess_urlstringcancel_urlstringmetadataobjectline_itemsarrayauto_invoicebooleanaccepted_methodsarrayline_items
Chaque ligne est soit un produit du catalogue, soit une ligne libre :
// Ligne catalogue — prix, TVA et désignation fixés côté serveur
{ "product_id": 4, "quantity": 2 }
{ "sku": "COURS-PY-001", "quantity": 1 }
// Ligne libre — designation et unit_price_cents requis
{ "designation": "Frais de port", "unit_price_cents": 500,
"vat_rate": 20, "quantity": 1, "unit": "u" }quantity (décimal > 0, défaut 1), unit (défaut "u"), vat_rate (défaut 20). Montant de la session = somme des lignes : round(unit_price_cents × quantity × (1 + vat_rate/100)). Une ligne catalogue introuvable, masquée ou en rupture de stock renvoie 409.
Idempotence
Passez un en-tête Idempotency-Key (≤ 128 caractères) pour rendre la création rejouable sans risque de doublon : si une session a déjà été créée avec la même clé d’idempotence par le même marchand dans les 24 dernières heures, elle est renvoyée telle quelle avec un statut 200 (au lieu de 201). À utiliser côté serveur : cet en-tête n’est pas autorisé par la configuration CORS des appels navigateur.
Réponse — 201 Created
{
"id": "ps_00000042",
"token": "9f2c44d1a07e35b8c6d2e91f54a3b7c8d0e6f1a2",
"url": "https://nexuspay.fr/checkout/9f2c44d1a07e35b8c6d2e91f54a3b7c8d0e6f1a2",
"amount": 4900,
"currency": "EUR",
"description": "Abonnement Pro — juin",
"customer_email": "client@exemple.fr",
"customer_name": null,
"customer_address": null,
"customer_postal_code": null,
"customer_city": null,
"customer_country": null,
"customer_siret": null,
"customer_vat_number": null,
"success_url": "https://votre-site.fr/merci?commande=1042",
"cancel_url": "https://votre-site.fr/panier",
"metadata": {},
"line_items": null,
"accepted_methods": ["cb"],
"status": "pending",
"auto_invoice": true,
"invoice_id": null,
"paid_at": null,
"expires_at": "2026-06-12T14:00:00+00:00",
"created_at": "2026-06-11T14:00:00+00:00"
}/api/v1/checkout/sessions/{id}sk_ uniquementRécupère une session. {id} est l’identifiant numérique de la session : pour "id": "ps_00000042", appelez /api/v1/checkout/sessions/42 (retirez le préfixe ps_ et les zéros de tête). Réponse : même objet que ci-dessus. 404 si la session n’existe pas ou appartient à un autre marchand.
/api/v1/checkout/sessions/{id}/refundsk_ uniquementRembourse une session payée (statut succeeded), totalement ou partiellement.
amountintegerreasonstring{
"refund_id": "re_3Nxxxxxxxxxxxxxx",
"refunded_amount": 4900,
"currency": "EUR",
"session_status": "refunded",
"fully_refunded": true
}Un remboursement total passe la session en refunded et la facture associée en « Remboursée ». Un remboursement partiel laisse la session en succeeded — un second remboursement partiel reste possible. Erreurs : 409 session non remboursable, 400 montant invalide, 503 Stripe non configuré, 502 échec Stripe.
Produits
CRUD sur votre catalogue, pour le synchroniser depuis votre CMS ou ERP. Toutes les routes exigent une clé sk_. Les champs sont en camelCase (contrairement aux sessions, en snake_case).
/api/v1/products?sku=… filtre exact · ?limit=1-500 (défaut 100)/api/v1/products/{id}/api/v1/productsname requis/api/v1/products/{id}mise à jour partielle/api/v1/products/{id}suppression logiqueChamps (création / mise à jour)
namestringcategorystringdescriptionstringskustringunitPriceCentsintegervatRatenumberstockTrackingbooleanstockQuantityinteger | nullavailableForCheckoutboolean{
"id": 4,
"name": "Cours Python — niveau 1",
"category": "Formation",
"description": null,
"sku": "COURS-PY-001",
"unitPriceCents": 12500,
"vatRate": 20,
"grossPriceCents": 15000,
"stockTracking": true,
"stockQuantity": 12,
"availableForCheckout": true,
"purchasable": true,
"createdAt": "2026-05-02T09:12:00+00:00",
"updatedAt": "2026-06-01T16:40:00+00:00"
}grossPriceCents = prix TTC calculé (round(unitPriceCents × (1 + vatRate/100))) — c’est ce montant qui est facturé quand une session référence le produit. purchasable est faux si le produit est masqué, supprimé ou en rupture. La liste exclut les produits supprimés et est triée par nom. DELETE renvoie { "deleted": true, "id": N }.
/api/v1/ n’autorisent que GET, POST et OPTIONS en cross-origin. PATCH et DELETE doivent être appelés depuis votre serveur — ce qui est de toute façon requis puisque la clé sk_ ne doit jamais transiter par le navigateur.Clients
CRUD sur votre base clients, pour la synchroniser depuis votre CRM, ERP ou plateforme e-commerce. Toutes les routes exigent une clé sk_ avec le scope customers:read ou customers:write. Champs en camelCase, comme les produits. Les clients créés ici apparaissent immédiatement dans le dashboard et sont utilisables pour la facturation.
/api/v1/customers?email=… | ?externalId=… · ?limit=1-500 · ?starting_after=<id>/api/v1/customers/{id}/api/v1/customersname requis (ou firstName + lastName)/api/v1/customers/{id}mise à jour partielle/api/v1/customers/{id}archivage (suppression logique)Champs (création / mise à jour)
namestringtypestringfirstName / lastNamestringemailstringphonestringsiren / siretstringtvaIntrastringexternalIdstringcountrystringtagsarrayaddressobject{
"id": 12,
"name": "Acme SAS",
"type": "company",
"firstName": null,
"lastName": null,
"email": "compta@acme.fr",
"phone": "+33 1 23 45 67 89",
"siren": "123456789",
"siret": "12345678900012",
"tvaIntra": "FR32123456789",
"externalId": "CRM-1042",
"country": "FR",
"tags": ["b2b", "saas"],
"address": {
"street": "10 rue de la Paix",
"postcode": "75002",
"city": "Paris"
},
"createdAt": "2026-06-12T09:00:00+00:00"
}La liste exclut les clients archivés, du plus récent au plus ancien (id décroissant). Pagination par curseur : la réponse porte has_more ; tant qu’il vaut true, rappelez la liste avec ?starting_after=<dernier id reçu>. Même mécanique sur les factures et le journal des webhooks. DELETE archive le client ({ "deleted": true, "id": N }) : il disparaît des listes mais reste référencé par ses factures passées.
Factures
Créez, émettez et envoyez des factures Nexus depuis votre ERP ou back-office : numérotation légale séquentielle, PDF Factur-X, page de paiement en ligne et écritures comptables — tout est généré par Nexus. Toutes les routes exigent une clé sk_ (scopes invoices:read / invoices:write). Champs en camelCase.
/api/v1/invoices?status= · ?customerId= · ?number= · ?limit= · ?starting_after=<id> (has_more)/api/v1/invoices/{id}avec lignes/api/v1/invoicesbrouillon — ou émise avec finalize: true/api/v1/invoices/{id}/finalizeémet (numérote) — idempotent/api/v1/invoices/{id}/sendenvoie par email (émet si brouillon)/api/v1/invoices/{id}/mark_paidrèglement hors ligne (virement…) — idempotent/api/v1/invoices/{id}/credit_noteavoir total (finalize: true pour l'émettre)/api/v1/invoices/{id}/pdfPDF Factur-X (factures émises)Cycle de vie
draft → open → paid | cancelled. Un draft n’a pas de numéro — il en reçoit un, séquentiel et définitif, à l’émission (finalize ou premier send). Une facture émise est immuable (art. 242 nonies A du CGI) : pas de PATCH ni de DELETE — une facture erronée se corrige par un avoir, depuis le dashboard.
Corps de la requête (création)
customerIdintegercustomerobjectlinesarrayfinalizebooleancurrencystringdescriptionstringdueDatestringpurchaseOrderRefstringpaymentTermsstringglobalDiscountCents / depositCentsintegerserviceStartDate / serviceEndDatestringacceptedMethodsarrayChaque ligne accepte les mêmes champs que le dashboard :
// Ligne libre — designation et unitPriceCents requis
{ "designation": "Développement — sprint 12", "quantity": 5,
"unitPriceCents": 60000, "vatRate": 20, "unit": "j" }
// Ligne catalogue — prix, TVA et désignation repris du produit
{ "productId": 4, "quantity": 2 }Idempotence
Comme pour les sessions : passez un en-tête Idempotency-Key (≤ 128 caractères) et un POST rejoué sous 24 h renvoie la facture déjà créée (200 au lieu de 201) — aucun doublon, même après un timeout réseau.
Réponse
{
"id": 412,
"number": "FAC-2026-0042",
"docType": "facture",
"status": "open",
"customerId": 12,
"customerName": "Acme SAS",
"description": "Prestations juin",
"currency": "EUR",
"subtotalHtCents": 300000,
"globalDiscountCents": 0,
"netCents": 300000,
"vatCents": 60000,
"grossCents": 360000,
"depositCents": 0,
"netToPayCents": 360000,
"acceptedMethods": ["cb"],
"purchaseOrderRef": "PO-2026-118",
"paymentTerms": null,
"issuedAt": "2026-06-12T10:00:00+00:00",
"dueDate": "2026-07-12T00:00:00+00:00",
"paidAt": null,
"serviceStartDate": null,
"serviceEndDate": null,
"paymentUrl": "https://nexuspay.fr/pay/9f2c44d1…",
"lines": [
{ "designation": "Développement — sprint 12", "description": null,
"unit": "j", "quantity": 5, "unitPriceCents": 60000,
"discountPercent": 0, "vatRate": 20, "lineTotalHtCents": 300000,
"kind": "service" }
]
}paymentUrl est la page de paiement hébergée de la facture (CB/crypto selon acceptedMethods) — partageable telle quelle avec votre client. POST /send envoie l’email (PDF Factur-X joint + bouton de paiement) au client de la facture, ou au destinataire passé dans { "to": "…" } (subject et message optionnels). Les webhooks invoice.created, invoice.finalized et invoice.sent accompagnent chaque étape — voir la section Webhooks.
Règlement hors ligne & avoirs
POST /mark_paid règle une facture payée hors plateforme — virement rapproché par votre ERP, chèque… Corps optionnel : { "reference": "VIR-…", "method": "virement|cheque|especes|autre", "paidAt": "…" }. Idempotent (une facture déjà payée est renvoyée telle quelle), émet invoice.paid (via: api + reference). POST /credit_note crée un avoir total sur une facture émise ({ "reason": "…", "finalize": true }) — l’avoir reprend les lignes de la facture, est numéroté AV-* à l’émission et passe les écritures d’extourne. L’avoir partiel n’est pas encore disponible. Une facture déjà intégralement soldée par avoir renvoie 409.
Devis
Générez des devis signables en ligne depuis votre CRM : numérotation DEV-* immédiate, PDF, page publique de signature électronique, conversion automatique en facture à l’acceptation. Scopes quotes:read / quotes:write, champs en camelCase, statuts draft → sent → accepted | refused | expired.
/api/v1/quotes?status= · ?customerId= · ?limit= · ?starting_after=<id> (has_more)/api/v1/quotes/{id}avec lignes/api/v1/quotescustomerId | customer inline + lines — numéroté immédiatement/api/v1/quotes/{id}/sendemail PDF + lien de signature ({ to, subject, message })/api/v1/quotes/{id}/convertaccepte + convertit en facture émise — idempotent/api/v1/quotes/{id}/pdfCorps de la requête (création)
customerId / customerinteger / objectlinesarrayvalidityDatestringdescription / currency / globalDiscountCents / acceptedMethods—La réponse expose publicUrl — la page hébergée « voir et signer » à partager avec votre client (la signature en ligne convertit automatiquement le devis en facture émise et emailée). POST /convert force la conversion côté serveur (commande validée par téléphone, par exemple) et renvoie invoiceId. La facture issue d’un devis de test hérite du mode test (numéro TEST-*, pas de comptabilité). Webhooks : quote.created, quote.sent, quote.accepted (avec invoice_id).
Encaissements (lecture)
/api/v1/payments?invoiceId= · ?network= · ?limit= · ?starting_after=<id> — scope payments:read/api/v1/payments/{id}Pour la réconciliation côté ERP sans dépendre des seuls webhooks : chaque paiement encaissé (CB Stripe, crypto on-chain, virement réconcilié) avec grossAmountCents / feeCents / netAmountCents, statut succeeded | pending | refunded, invoiceId / invoiceNumber, stripePaymentIntentId ou txHash. Les paiements étant des flux réels, une clé de test reçoit toujours une liste vide.
Pages hébergées
/checkout/{token} — paiement d’une session
La page retournée dans url à la création de session. Paiement par carte (Stripe, 3-D Secure), coordonnées pré-remplies depuis les champs customer_*, aux couleurs du marchand. À l’encaissement :
- facture générée automatiquement (si
auto_invoice), fiche client créée ou enrichie, reçu PDF envoyé àcustomer_email, stock décrémenté ; - redirection du navigateur vers
success_urltelle quelle, sans aucun paramètre ajouté (ni token ni identifiant de session). Pour corréler le retour, incluez votre propre référence dans l’URL (?commande=1042) et confirmez par webhook ou parGET /api/v1/checkout/sessions/{id}; cancel_urln’est affichée que comme lien « Retourner sur le site marchand » lorsque la session est expirée ou en échec — il n’y a pas de bouton annuler pendant le paiement.
Endpoints publics par token (sans clé)
La page hébergée s’appuie sur trois endpoints publics, résolus par le token de la session (40 caractères hexadécimaux). Utiles si vous construisez votre propre UI de statut — réponses en camelCase :
/api/checkout/{token}détail public de la session + branding marchand/api/checkout/{token}/stripe-intentinitie le paiement carte (usage interne de la page)/api/checkout/{token}/statuspolling léger du statut{ "status": "succeeded", "paidAt": "2026-06-11T14:03:21+00:00", "invoiceId": 318 }/pay/{token} — paiement d’une facture
Flux distinct des sessions : chaque facture Nexus possède sa page publique de règlement (carte ou crypto on-chain), partagée depuis le dashboard. Pas de success_url/cancel_url — le suivi passe par les webhooks payment.cb.succeeded et payment.crypto.received.
SDK JS — bouton « Payer avec Nexus »
nexus.js est un script léger, sans dépendance, qui affiche un bouton de paiement et redirige vers le checkout hébergé. Deux modes selon votre besoin.
Mode 1 — Prix fixe, sans serveur
Pour vendre un produit de votre catalogue (formation, abonnement, prestation) : six lignes dans n’importe quelle page (HTML, WordPress, Wix, Webflow), aucun backend. La clé publique pk_ est sûre côté navigateur — le prix vient du catalogue Nexus, un visiteur qui lit la clé ne peut rien fausser.
<div id="nexus-pay"></div>
<script src="https://nexuspay.fr/sdk/nexus.js"></script>
<script>
Nexus.createPaymentButton('#nexus-pay', {
publishableKey: 'pk_live_…',
sku: 'FORMATION-01', // ou productId: 4 ; quantity optionnel
successUrl: 'https://votre-site.fr/merci',
});
</script>Au clic, le SDK crée la session directement contre l’API Nexus et redirige vers le checkout. Le produit doit exister au catalogue et être disponible (sinon 409). Toute tentative de fixer un prix avec une pk_ est refusée (403) — c’est le mode serveur qu’il faut alors.
Mode 2 — Montant variable, via votre serveur
Pour un panier ou un montant calculé (e-commerce) : la clé secrète reste sur votre serveur. Le SDK appelle un endpoint de votre backend, qui crée la session avec sa clé sk_ et renvoie l’URL de checkout.
<div id="nexus-pay"></div>
<script src="https://nexuspay.fr/sdk/nexus.js"></script>
<script>
Nexus.createPaymentButton('#nexus-pay', {
sessionUrl: '/api/create-nexus-session', // VOTRE endpoint serveur
payload: { orderId: '1042' }, // corps JSON transmis tel quel
label: 'Payer 49,00 €', // optionnel
});
</script>Au clic, le SDK fait un POST JSON sur sessionUrl et attend en retour une URL de checkout — soit une chaîne, soit un objet portant url (la réponse brute de l’API Nexus convient). Alternative : passez fetchSession, une fonction async qui renvoie cette URL. Options : newTab (ouvre un onglet au lieu de rediriger), onError (callback d’échec).
app.post('/api/create-nexus-session', async (req, res) => {
const r = await fetch('https://api.nexuspay.fr/api/v1/checkout/sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.NEXUS_SECRET_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': 'commande-' + req.body.orderId,
},
body: JSON.stringify({
amount: 4900, // prix calculé CÔTÉ SERVEUR
currency: 'EUR',
description: 'Commande #' + req.body.orderId,
metadata: { order_id: req.body.orderId },
success_url: 'https://votre-site.fr/merci?commande=' + req.body.orderId,
}),
});
if (!r.ok) return res.status(502).json({ error: 'payment init failed' });
const session = await r.json();
res.json({ url: session.url });
});<?php
// create-nexus-session.php
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$orderId = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) ($input['orderId'] ?? ''));
$ch = curl_init('https://api.nexuspay.fr/api/v1/checkout/sessions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('NEXUS_SECRET_KEY'),
'Content-Type: application/json',
'Idempotency-Key: commande-' . $orderId,
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 4900, // prix calculé CÔTÉ SERVEUR
'currency' => 'EUR',
'description' => 'Commande #' . $orderId,
'metadata' => ['order_id' => $orderId],
'success_url' => 'https://votre-site.fr/merci?commande=' . $orderId,
]),
]);
$session = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
header('Content-Type: application/json');
if ($status >= 400 || !isset($session['url'])) {
http_response_code(502);
echo json_encode(['error' => 'payment init failed']);
exit;
}
echo json_encode(['url' => $session['url']]);POST https://api.nexuspay.fr/api/v1/checkout/sessions avec Authorization: Bearer pk_… et { "line_items": [{ "sku": "FORMATION-01" }] } — vous pouvez l’appeler directement si vous préférez votre propre bouton. Une pk_ ne peut référencer que le catalogue (jamais une ligne à prix libre) : le prix est garanti côté serveur.SDK serveur — Node.js
Côté serveur, le package nexuspay (Node ≥ 18, zéro dépendance, types inclus) couvre toute l’API v1 : sessions, produits, clients, factures, webhooks — avec idempotence et vérification de signature intégrées. Il vit dans le repo (sdk/node), en attendant sa publication npm.
import { Nexus, verifyWebhookSignature } from 'nexuspay';
const nexus = new Nexus({ apiKey: process.env.NEXUS_SECRET_KEY });
const invoice = await nexus.invoices.create({
customer: { name: 'Acme SAS', email: 'compta@acme.fr' },
lines: [{ designation: 'Prestation', unitPriceCents: 10000, vatRate: 20 }],
finalize: true,
}, { idempotencyKey: 'fac-2026-118' });
await nexus.invoices.send(invoice.id);
// Côté réception de webhook (corps BRUT requis) :
const ok = verifyWebhookSignature({
payload: rawBody,
header: req.headers['x-nexus-signature'],
secret: process.env.NEXUS_WEBHOOK_SECRET,
});SDK serveur — PHP
Même couverture en PHP (≥ 8.1, zéro dépendance) : nexuspay/nexuspay-php dans le repo (sdk/php) — ressources $nexus->invoices->create(), actions ->action($id, "mark_paid", […]), et Nexus::verifyWebhookSignature() pour vos webhooks (corps brut de php://input).
La spécification OpenAPI 3.1 décrit l’intégralité de l’API v1 (26 endpoints, schémas, enveloppe des webhooks) — importez-la dans Postman ou Insomnia. Pour les autres langages (Python, Go, Ruby…), générez un client typé en une commande :
openapi-generator-cli generate \ -i https://nexuspay.fr/openapi.yaml \ -g python -o ./nexuspay-python
Widget iframe
Version épurée et embarquable de la page de paiement : résumé de la session (marchand, montant, description) et bouton ouvrant le checkout complet dans un nouvel onglet — le paiement carte 3-D Secure exige une page de premier niveau. Le widget surveille ensuite le statut et affiche la confirmation dans l’iframe.
<iframe src="https://nexuspay.fr/embed/checkout/VOTRE_TOKEN_DE_SESSION" style="width:100%;max-width:420px;height:280px;border:0;border-radius:16px" title="Paiement Nexus"></iframe>
Le token est celui retourné à la création de session (champ token). Le widget poste au parent des messages postMessage à chaque changement de statut — aucune donnée sensible :
window.addEventListener('message', (e) => {
if (e.data && e.data.type === 'nexus.checkout' && e.data.status === 'succeeded') {
// rafraîchir la page, afficher la confirmation…
}
});Webhooks
Nexus notifie votre serveur par POST JSON à chaque événement. Configurez vos endpoints dans le dashboard (Développeurs & Webhooks) ou par API (ci-dessous) : URL https obligatoire (hôtes internes/privés refusés), secret de signature de 64 caractères hexadécimaux affiché une seule fois. Un endpoint sans liste d’événements sélectionnés reçoit tous les événements.
Gérer les endpoints par API
Pour provisionner vos webhooks sans passer par le dashboard (déploiement automatisé, multi-environnements) — scopes webhooks:read / webhooks:write :
/api/v1/webhook_endpoints/api/v1/webhook_endpointsurl (https) requis · name, events optionnels · renvoie signingSecret UNE fois/api/v1/webhook_endpoints/{id}enabled / name / events (l’url ne se modifie pas : supprimer + recréer)/api/v1/webhook_endpoints/{id}/api/v1/webhook_endpoints/{id}/testlivraison d’essai signée (data.livemode: false)/api/v1/webhook_endpoints/{id}/rotate_secretnouveau secret (affiché une fois) — l’ancien cesse de signer/api/v1/webhook_deliveriesjournal · ?event= · ?endpointId= · ?limit=1-100 · ?starting_after=<id>/api/v1/webhook_deliveries/{id}/replayrejoue une livraison échouéecurl -X POST https://api.nexuspay.fr/api/v1/webhook_endpoints \
-H "Authorization: Bearer sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.votre-domaine.fr/nexus/webhooks",
"name": "ERP production",
"events": ["invoice.paid", "payment.session.succeeded"]
}'Enveloppe
{
"event": "payment.session.succeeded",
"data": { … }, // payload propre à l'événement
"timestamp": 1781445801, // epoch Unix (entier)
"delivery_id": 9182 // identifiant unique de livraison
}| En-tête | Contenu |
|---|---|
X-Nexus-Signature | Signature : t=<unix>,v1=<HMAC-SHA256 hex> |
X-Nexus-Event | Nom de l’événement |
X-Nexus-Delivery | Identifiant de livraison (dédupliquez avec) |
Content-Type | application/json |
Vérifier la signature
v1 = HMAC-SHA256 hexadécimal du corps brut de la requête, calculé avec le secret de signature de l’endpoint.
t de l’en-tête n’est pas inclus dans le message signé — ne préfixez pas le corps par t. à la façon de Stripe, la vérification échouerait. Signez le corps brut, seul, tel que reçu (avant tout parsing JSON).const crypto = require('crypto');
const express = require('express');
const app = express();
// IMPORTANT : corps BRUT — pas de express.json() sur cette route.
app.post('/webhooks/nexus', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-Nexus-Signature') || '';
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const expected = crypto
.createHmac('sha256', process.env.NEXUS_WEBHOOK_SECRET)
.update(req.body) // Buffer du corps brut
.digest('hex');
const provided = parts.v1 || '';
const valid =
provided.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
if (!valid) return res.status(400).send('invalid signature');
const { event, data, delivery_id } = JSON.parse(req.body.toString('utf8'));
// Dédupliquez par delivery_id : une livraison peut être rejouée.
if (event === 'payment.session.succeeded') {
// livrer la commande : data.session_id, data.metadata.order_id…
}
res.sendStatus(200); // répondez 2xx rapidement, traitez en asynchrone
});<?php
// webhook-nexus.php
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_NEXUS_SIGNATURE'] ?? '';
$parts = [];
foreach (explode(',', $header) as $kv) {
[$k, $v] = array_pad(explode('=', $kv, 2), 2, '');
$parts[$k] = $v;
}
$expected = hash_hmac('sha256', $rawBody, getenv('NEXUS_WEBHOOK_SECRET'));
if (!hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(400);
exit('invalid signature');
}
$payload = json_decode($rawBody, true);
// Dédupliquez par $payload['delivery_id'] : une livraison peut être rejouée.
if (($payload['event'] ?? '') === 'payment.session.succeeded') {
// livrer la commande : $payload['data']['session_id'], …
}
http_response_code(200);Événements
payment.session.createdSession de paiement créée via l’API (non émis lors d’un rejeu idempotent).{
"session_id": "ps_00000042",
"session_token": "9f2c44d1…",
"amount": 4900,
"currency": "EUR",
"description": "Abonnement Pro — juin",
"customer_email": "client@exemple.fr",
"customer_name": null,
"metadata": { "order_id": "1042" },
"checkout_url": "https://nexuspay.fr/checkout/9f2c44d1…",
"expires_at": "2026-06-12T14:00:00+00:00"
}payment.session.succeededSession payée — l’événement de référence pour livrer la commande.{
"session_id": "ps_00000042",
"session_token": "9f2c44d1…",
"amount": 4900,
"currency": "EUR",
"customer_email": "client@exemple.fr",
"customer_name": "Jeanne Martin",
"description": "Abonnement Pro — juin",
"metadata": { "order_id": "1042" },
"invoice_id": 318,
"paid_at": "2026-06-11T14:03:21+00:00",
"payment_intent": "pi_3Nxxxxxxxxxxxxxx"
}payment.session.failedTentative de paiement refusée (carte déclinée, 3-D Secure échoué…) — la session passe en failed. Utile pour libérer le panier ou relancer l’acheteur sans poller la session.{
"session_id": "ps_00000042",
"session_token": "9f2c44d1…",
"amount": 4900,
"currency": "EUR",
"metadata": { "order_id": "1042" },
"reason": "Your card was declined."
}invoice.created / invoice.finalized / invoice.sent / invoice.cancelledCycle de vie d’une facture : créée (brouillon ou émise, avoirs inclus — parent_invoice_id), émise (numérotée), envoyée par email (champ recipient), annulée. Émis pour les factures API comme pour les actions du dashboard.{
"invoice_id": 412,
"number": "FAC-2026-0042",
"status": "open",
"doc_type": "facture",
"amount": 360000,
"currency": "EUR",
"customer_id": 12,
"customer_email": "compta@acme.fr",
"due_date": "2026-07-12T00:00:00+00:00"
}quote.created / quote.sent / quote.acceptedCycle de vie d’un devis créé via l’API : créé (numéroté), envoyé par email (recipient), accepté/converti (invoice_id de la facture émise).{
"quote_id": 87,
"number": "DEV-2026-0012",
"status": "accepted",
"amount": 360000,
"currency": "EUR",
"customer_id": 12,
"customer_email": "compta@acme.fr",
"validity_date": "2026-07-12",
"livemode": true,
"invoice_id": 412
}invoice.paidFacture passée en « payée » — quel que soit le canal : paiement CB sur le lien /pay (via: cb), session de checkout avec facture auto-générée (via: session), marquage manuel au dashboard (via: manual), ou règlement hors ligne par API (via: api, avec method et reference). Émis UNE fois, au règlement complet (la dernière échéance d’un plan le déclenche).{
"invoice_id": 412,
"number": "FAC-2026-0042",
"status": "paid",
"doc_type": "facture",
"amount": 360000,
"currency": "EUR",
"customer_id": 12,
"customer_email": "compta@acme.fr",
"due_date": "2026-07-12T00:00:00+00:00",
"paid_at": "2026-06-12T14:03:21+00:00",
"payment_intent": "pi_3Nxxxxxxxxxxxxxx",
"via": "cb"
}payment.cb.succeededPaiement carte encaissé sur une facture (page /pay ou session).{
"invoiceId": "online_pi_3Nxxxxxxxxxxxxxx",
"paymentIntent": "pi_3Nxxxxxxxxxxxxxx",
"grossAmount": 4900,
"nexusFee": 88,
"merchantNet": 4812,
"currency": "EUR"
}payment.refundedRemboursement effectué (total ou partiel).{
"invoiceId": "online_pi_3Nxxxxxxxxxxxxxx",
"refund_id": "re_3Nxxxxxxxxxxxxxx",
"amount": 2000,
"total_refunded": 4900,
"currency": "EUR",
"fully_refunded": true
}amount est le montant du remboursement qui déclenche l’événement, pas le cumul — créditez votre client de ce montant, même en cas de remboursements partiels successifs. Le cumul est fourni dans total_refunded (équivalent de amount_refunded chez Stripe) et fully_refunded indique si le paiement est intégralement remboursé. Dédupliquez par refund_id : chaque remboursement émet son propre événement.payment.crypto.receivedPaiement crypto on-chain confirmé sur une facture.{
"invoiceId": "FAC-2026-0042",
"txHash": "0x6f8a…",
"grossAmount": 4900,
"nexusFee": 49,
"merchantNet": 4851,
"currency": "ETH",
"network": "linea"
}dispute.created / dispute.updated / dispute.closedLitige (chargeback) ouvert, mis à jour ou clôturé sur un paiement carte.{
"stripeDisputeId": "dp_1Nxxxxxxxxxxxxxx",
"amount": 4900,
"currency": "EUR",
"reason": "fraudulent",
"status": "needs_response",
"evidenceDueBy": "2026-06-25T23:59:59+00:00",
"invoiceId": 318
}Livraison & retries
- Succès = toute réponse HTTP 2xx. Timeout : 10 secondes.
- En cas d’échec, relances automatiques avec backoff :
1 min → 5 min → 30 min → 2 h → 12 h → 24 h(6 tentatives au total), puis la livraison est marquéefailed. - Rejeu manuel possible à tout moment depuis le journal de livraisons du dashboard.
- Concevez votre handler idempotent : dédupliquez par
delivery_id(un rejeu renvoie le même id) et, pour les paiements, parsession_id.
Limites de débit
L’API limite le nombre de requêtes par adresse IP, en fenêtre glissante de 5 minutes :
| Périmètre | Limite |
|---|---|
| Ensemble de l’API (dont /api/v1/) | 300 requêtes / 5 min par IP |
| Endpoints d’authentification (connexion, OTP) | 30 requêtes / 5 min par IP |
En cas de dépassement, la requête reçoit 429 avec un en-tête Retry-After (nombre de secondes à attendre) :
Retry-After: 142
{ "error": "Trop de requêtes. Réessayez plus tard." }Respectez Retry-After avant de réémettre et lissez vos rafales (création de sessions en masse, synchronisation de catalogue). La réception de vos webhooks par Nexus n’est pas concernée — la limite porte sur les appels entrants vers l’API.
Erreurs
Les erreurs sont renvoyées en JSON : { "error": "message" } avec le code HTTP approprié. Quelques cas notables :
| Code | Signification |
|---|---|
| 400 | Requête invalide — JSON malformé, amount ≤ 0, currency non ISO-3, line_items invalides… |
| 401 | Clé API absente, invalide ou révoquée. |
| 403 | Clé pk_ avec montant libre (un produit du catalogue est requis), ou scope insuffisant ({ "error": "insufficient_scope" }). |
| 404 | Ressource introuvable ou appartenant à un autre marchand (session, produit, token de paiement). |
| 409 | Conflit d’état — produit introuvable/masqué/en rupture, SKU déjà utilisé (existing_product_id fourni), session non payable ou non remboursable. |
| 429 | Limite de débit dépassée — réessayez après le délai indiqué par l’en-tête Retry-After (voir Limites de débit). |
| 502 | Échec d’un appel au prestataire de paiement (le message reste générique). |
| 503 | Paiement par carte non configuré pour ce marchand. |
Les sessions créées avec Idempotency-Key renvoient 200 (au lieu de 201) lorsqu’une session existante est rejouée. En cas de doute sur l’état d’une session après un timeout réseau, rejouez la création avec la même clé d’idempotence ou lisez la session avec GET /api/v1/checkout/sessions/{id}.