API v1 · REST · JSON

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.

Base URL : https://api.nexuspay.fr·Spec OpenAPI 3.1
Guide

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.

1Bouton sans serveur

Prix fixe du catalogue (formation, abonnement). Six lignes de HTML, clé pk_, aucun backend. → SDK, mode 1

~5 minutes
2Bouton + endpoint

Montant variable (panier e-commerce). Le snippet nexus.js appelle votre serveur, qui fixe le prix avec sa clé sk_. → SDK, mode 2

~1 heure
3API complète

ERP, SaaS, CRM. Facturation, devis et réconciliation automatisés, webhooks signés, mode test étanche, SDK Node/PHP.

1–3 jours

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. 1. Deux clés. Créez une clé sk_test_ (développement) et une sk_live_ (coffre, pour plus tard). Restreignez leurs scopes au strict besoin. Côté ERP : NEXUS_API_KEY=sk_test_….
  2. 2. Le webhook d’abord. Créez un endpoint, stockez son signingSecret, branchez verifyWebhookSignature (corps brut, réponse 2xx rapide, déduplication par X-Nexus-Delivery). Validez avec POST /webhook_endpoints/{id}/test.
  3. 3. Développez en mode test. Tout est étanche : numéros TEST-*, zéro comptabilité, aucun argent réel. Construisez vos flux (customers, invoices create/send/mark_paid/credit_note, quotes), réglez les sessions de test par simulate_payment.
  4. 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 dans webhook_deliveries).
  5. 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.
Le paiement carte en mode test
En test, l’argent ne bouge jamais : on teste le flux par 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.
Guide

Démarrage en 5 minutes

1

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.

2

Créez une session de paiement (côté serveur)

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

3

Redirigez votre client

Node.js — fetch
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)
4

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

Sécurité

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

Clé secrète — sk_
Réservée à votre serveur. Accès complet : sessions à montant libre, lecture, remboursements, CRUD produits. Stockée hachée (bcrypt) chez Nexus, affichée une seule fois à la création. Ne l’exposez jamais dans un navigateur, une app mobile ou un dépôt git.
Clé publique — pk_
Utilisable côté navigateur. Elle ne peut créer que des sessions référençant un produit du catalogue (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 :

ScopeEndpoints autorisés
checkout:writePOST /api/v1/checkout/sessions
checkout:readGET /api/v1/checkout/sessions/{id}
refunds:writePOST /api/v1/checkout/sessions/{id}/refund
products:readGET /api/v1/products[/{id}]
products:writePOST | PATCH | DELETE /api/v1/products[/{id}]
customers:readGET /api/v1/customers[/{id}]
customers:writePOST | PATCH | DELETE /api/v1/customers[/{id}]
invoices:readGET /api/v1/invoices[/{id}[/pdf]]
invoices:writePOST /api/v1/invoices[/{id}/finalize | send]
webhooks:readGET /api/v1/webhook_endpoints | webhook_deliveries
webhooks:writePOST | PATCH | DELETE /api/v1/webhook_endpoints[/{id}] · replay
quotes:readGET /api/v1/quotes[/{id}[/pdf]]
quotes:writePOST /api/v1/quotes[/{id}/send | convert]
payments:readGET /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 :

403 Forbidden
{ "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 :

ComportementMode test
Clients & facturesVisibles uniquement par les clés test — jamais dans le dashboard ni via une clé live (et inversement).
NumérotationPréfixe TEST- sur une séquence à part : la numérotation légale ne consomme aucun numéro.
Comptabilité & fiscalAucune écriture comptable, exclues de la CA3/DEB/DES, des clôtures et des agrégats.
PaiementPas 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.)

Référence

Sessions de paiement

POST/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 : pendingsucceeded | failed | expired | refunded.

Corps de la requête

amountinteger
Montant TTC en centimes (> 0). Requis sauf si un produit du catalogue ou line_items est fourni — dans ce cas le montant est calculé côté serveur et la valeur envoyée est ignorée.
currencystring
Code ISO 4217 sur 3 lettres. Défaut : "EUR".
descriptionstring
Libellé affiché sur la page de paiement (tronqué à 255 caractères). Défaut : nom du produit catalogue le cas échéant.
customer_emailstring
E-mail de l’acheteur — pré-rempli sur la page de paiement, reçoit le reçu PDF.
customer_namestring
Nom de l’acheteur.
customer_addressobject
Adresse : { "street" | "line1", "postal_code" | "postcode", "city", "country" }. Alternative : champs plats customer_street, customer_postal_code, customer_city, customer_country.
customer_siretstring
SIRET de l’acheteur (B2B, repris sur la facture).
customer_vat_numberstring
N° TVA intracommunautaire (alias accepté : customer_vat).
customer_external_idstring
Votre identifiant client — replié dans metadata.customer_external_id, utilisé pour rapprocher la fiche client Nexus.
success_urlstring
URL de redirection après paiement réussi (max 500 caractères). Redirigée telle quelle, sans paramètre ajouté — incluez votre propre référence de commande.
cancel_urlstring
URL « retour marchand » affichée en lien si la session est expirée ou en échec (max 500 caractères).
metadataobject
Objet libre, restitué dans les réponses et les webhooks. Clés spéciales : product_id (ou productId) et sku référencent un produit du catalogue — le prix TTC du produit remplace alors amount.
line_itemsarray
Panier multi-lignes (max 50) — prioritaire sur amount et metadata.product_id. Voir ci-dessous.
auto_invoiceboolean
Génère automatiquement la facture à l’encaissement. Défaut : true (seul false littéral désactive).
accepted_methodsarray
Sous-ensemble de ["cb", "crypto"]. Défaut : ["cb"].

line_items

Chaque ligne est soit un produit du catalogue, soit une ligne libre :

Exemples de lignes
// 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

application/json
{
  "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"
}
GET/api/v1/checkout/sessions/{id}sk_ uniquement

Ré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.

POST/api/v1/checkout/sessions/{id}/refundsk_ uniquement

Rembourse une session payée (statut succeeded), totalement ou partiellement.

amountinteger
Montant à rembourser en centimes. Optionnel — défaut : montant total. Doit être > 0 et ≤ au montant de la session.
reasonstring
Optionnel : "requested_by_customer", "duplicate" ou "fraudulent".
Réponse — 200 OK
{
  "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.

Référence

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

GET/api/v1/products?sku=… filtre exact · ?limit=1-500 (défaut 100)
GET/api/v1/products/{id}
POST/api/v1/productsname requis
PATCH/api/v1/products/{id}mise à jour partielle
DELETE/api/v1/products/{id}suppression logique

Champs (création / mise à jour)

namestring
Nom du produit. Requis à la création.
categorystring
Catégorie libre.
descriptionstring
Description.
skustring
Votre référence. Unique par marchand : une collision à la création renvoie 409 { "error": "sku already in use", "existing_product_id": N } — idéal pour faire un upsert (POST puis PATCH sur l’id existant).
unitPriceCentsinteger
Prix unitaire HT en centimes.
vatRatenumber
Taux de TVA en % (ex. 20).
stockTrackingboolean
Active le suivi de stock.
stockQuantityinteger | null
Quantité en stock. null = illimité.
availableForCheckoutboolean
Produit vendable via l’API checkout.
Réponse produit
{
  "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 }.

CORS
Les routes /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.
Référence

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.

GET/api/v1/customers?email=… | ?externalId=… · ?limit=1-500 · ?starting_after=<id>
GET/api/v1/customers/{id}
POST/api/v1/customersname requis (ou firstName + lastName)
PATCH/api/v1/customers/{id}mise à jour partielle
DELETE/api/v1/customers/{id}archivage (suppression logique)

Champs (création / mise à jour)

namestring
Raison sociale ou nom complet. Requis à la création — pour un particulier, firstName + lastName suffisent (le name est composé automatiquement).
typestring
"company" (défaut) ou "individual".
firstName / lastNamestring
Prénom / nom (particuliers).
emailstring
E-mail — destinataire par défaut des factures envoyées par API.
phonestring
Téléphone.
siren / siretstring
Identifiants entreprise (B2B, repris sur les factures).
tvaIntrastring
N° TVA intracommunautaire.
externalIdstring
Votre identifiant CRM/ERP. Une collision à la création renvoie 409 { "error": "externalId already in use", "existing_customer_id": N } — idéal pour faire un upsert (POST puis PATCH sur l’id existant).
countrystring
Code pays ISO-2 (défaut "FR").
tagsarray
Liste de tags libres (strings).
addressobject
{ "street", "postcode", "city" }.
Réponse client
{
  "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.

Référence

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.

GET/api/v1/invoices?status= · ?customerId= · ?number= · ?limit= · ?starting_after=<id> (has_more)
GET/api/v1/invoices/{id}avec lignes
POST/api/v1/invoicesbrouillon — ou émise avec finalize: true
POST/api/v1/invoices/{id}/finalizeémet (numérote) — idempotent
POST/api/v1/invoices/{id}/sendenvoie par email (émet si brouillon)
POST/api/v1/invoices/{id}/mark_paidrèglement hors ligne (virement…) — idempotent
POST/api/v1/invoices/{id}/credit_noteavoir total (finalize: true pour l'émettre)
GET/api/v1/invoices/{id}/pdfPDF Factur-X (factures émises)

Cycle de vie

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

customerIdinteger
Id d’un client existant. Alternative : customer (objet inline ci-dessous). L’un des deux est requis.
customerobject
Client inline : retrouvé par externalId, puis par email ; créé sinon (name requis, ou firstName + lastName). Mêmes champs que l’API Clients.
linesarray
Lignes de facturation (requis, non vide). Validation stricte : une ligne invalide renvoie 422 avec son index — rien n’est ignoré silencieusement.
finalizeboolean
true = émet immédiatement (numérotation + écritures comptables + lien de paiement). Défaut : false (brouillon).
currencystring
Code ISO 4217. Défaut : "EUR".
descriptionstring
Objet de la facture.
dueDatestring
Échéance (ISO 8601).
purchaseOrderRefstring
Référence de commande / bon de commande.
paymentTermsstring
Conditions de paiement affichées.
globalDiscountCents / depositCentsinteger
Remise globale / acompte, en centimes.
serviceStartDate / serviceEndDatestring
Période de prestation (YYYY-MM-DD).
acceptedMethodsarray
Sous-ensemble de ["cb", "crypto", "especes"].

Chaque ligne accepte les mêmes champs que le dashboard :

Exemples de lignes
// 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

POST /api/v1/invoices (finalize: true) — 201 Created
{
  "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.

Référence

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.

GET/api/v1/quotes?status= · ?customerId= · ?limit= · ?starting_after=<id> (has_more)
GET/api/v1/quotes/{id}avec lignes
POST/api/v1/quotescustomerId | customer inline + lines — numéroté immédiatement
POST/api/v1/quotes/{id}/sendemail PDF + lien de signature ({ to, subject, message })
POST/api/v1/quotes/{id}/convertaccepte + convertit en facture émise — idempotent
GET/api/v1/quotes/{id}/pdf

Corps de la requête (création)

customerId / customerinteger / object
Comme l’API Factures : id existant, ou client inline retrouvé par externalId puis email (créé sinon).
linesarray
Requis, non vide — { designation*, unitPriceCents*, quantity, vatRate, unit, discountPercent, description }. Validation stricte (422 ciblée).
validityDatestring
Date de validité (YYYY-MM-DD) — le devis devient expired au-delà.
description / currency / globalDiscountCents / acceptedMethods
Comme l’API Factures.

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)

GET/api/v1/payments?invoiceId= · ?network= · ?limit= · ?starting_after=<id> — scope payments:read
GET/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.

Parcours acheteur

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_url telle 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 par GET /api/v1/checkout/sessions/{id} ;
  • cancel_url n’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 :

GET/api/checkout/{token}détail public de la session + branding marchand
POST/api/checkout/{token}/stripe-intentinitie le paiement carte (usage interne de la page)
GET/api/checkout/{token}/statuspolling léger du statut
GET /api/checkout/{token}/status — 200
{ "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.

Intégration

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.

Page HTML — zéro serveur
<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.

1 · Votre page HTML
<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).

2 · Endpoint serveur — Node.js / Express
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 });
});
2 · Endpoint serveur — PHP
<?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']]);
Sous le capot du mode 1
Le mode sans serveur fait exactement un 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.

Node.js — nexuspay
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 :

Python — client généré depuis la spec
openapi-generator-cli generate \
  -i https://nexuspay.fr/openapi.yaml \
  -g python -o ./nexuspay-python
Intégration

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.

Intégration
<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 :

Écoute côté page hôte
window.addEventListener('message', (e) => {
  if (e.data && e.data.type === 'nexus.checkout' && e.data.status === 'succeeded') {
    // rafraîchir la page, afficher la confirmation…
  }
});
Référence

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 :

GET/api/v1/webhook_endpoints
POST/api/v1/webhook_endpointsurl (https) requis · name, events optionnels · renvoie signingSecret UNE fois
PATCH/api/v1/webhook_endpoints/{id}enabled / name / events (l’url ne se modifie pas : supprimer + recréer)
DELETE/api/v1/webhook_endpoints/{id}
POST/api/v1/webhook_endpoints/{id}/testlivraison d’essai signée (data.livemode: false)
POST/api/v1/webhook_endpoints/{id}/rotate_secretnouveau secret (affiché une fois) — l’ancien cesse de signer
GET/api/v1/webhook_deliveriesjournal · ?event= · ?endpointId= · ?limit=1-100 · ?starting_after=<id>
POST/api/v1/webhook_deliveries/{id}/replayrejoue une livraison échouée
curl — créer un endpoint
curl -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

Corps de chaque livraison
{
  "event": "payment.session.succeeded",
  "data": { … },                 // payload propre à l'événement
  "timestamp": 1781445801,       // epoch Unix (entier)
  "delivery_id": 9182            // identifiant unique de livraison
}
En-têteContenu
X-Nexus-SignatureSignature : t=<unix>,v1=<HMAC-SHA256 hex>
X-Nexus-EventNom de l’événement
X-Nexus-DeliveryIdentifiant de livraison (dédupliquez avec)
Content-Typeapplication/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.

Différence avec Stripe
Le timestamp 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).
Node.js — Express
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
<?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).
data
{
  "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.
data
{
  "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.
data
{
  "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.
data
{
  "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).
data
{
  "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).
data
{
  "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).
data
{
  "invoiceId": "online_pi_3Nxxxxxxxxxxxxxx",
  "paymentIntent": "pi_3Nxxxxxxxxxxxxxx",
  "grossAmount": 4900,
  "nexusFee": 88,
  "merchantNet": 4812,
  "currency": "EUR"
}
payment.refundedRemboursement effectué (total ou partiel).
data
{
  "invoiceId": "online_pi_3Nxxxxxxxxxxxxxx",
  "refund_id": "re_3Nxxxxxxxxxxxxxx",
  "amount": 2000,
  "total_refunded": 4900,
  "currency": "EUR",
  "fully_refunded": true
}
payment.refunded : amount = remboursement déclencheur
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.
data
{
  "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.
data
{
  "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ée failed.
  • 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, par session_id.
Référence

Limites de débit

L’API limite le nombre de requêtes par adresse IP, en fenêtre glissante de 5 minutes :

PérimètreLimite
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) :

429 Too Many Requests
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.

Référence

Erreurs

Les erreurs sont renvoyées en JSON : { "error": "message" } avec le code HTTP approprié. Quelques cas notables :

CodeSignification
400Requête invalide — JSON malformé, amount ≤ 0, currency non ISO-3, line_items invalides…
401Clé API absente, invalide ou révoquée.
403Clé pk_ avec montant libre (un produit du catalogue est requis), ou scope insuffisant ({ "error": "insufficient_scope" }).
404Ressource introuvable ou appartenant à un autre marchand (session, produit, token de paiement).
409Conflit d’état — produit introuvable/masqué/en rupture, SKU déjà utilisé (existing_product_id fourni), session non payable ou non remboursable.
429Limite 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).
503Paiement 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}.

Une question, un cas non couvert ? Écrivez-nous depuis le dashboard.
Réserver une démo