Aller au contenu
Documentation
Sur cette page

Guide d’intégration

Documentation développeur

Créez une facture, envoyez le client vers la page de paiement et recevez un webhook signé une fois le paiement effectué. Commencez par les quatre étapes ci-dessous.

  1. Copiez la clé API de votre projet.
  2. Signez les requêtes d’écriture.
  3. Créez votre première facture.
  4. Vérifiez le webhook de paiement.

Ce qui est pris en charge

La passerelle accepte USDT et USDC. Ce tableau correspond à GET /v1/services, vérifié le 4 novembre 2026. Consultez le catalogue à jour avant de construire votre sélecteur de moyens de paiement.

ActifRéseauConfirmations
USDTTron20
USDTEthereum12
USDTSolana32
USDTPolygon8
USDTArbitrum12
USDTTON5
USDCEthereum12
USDCSolana32
USDCPolygon8
USDCBase12
USDCArbitrum12

Les paiements sont dépositaires : les fonds arrivent sur des adresses contrôlées par la plateforme. Votre solde marchand correspond au montant qui vous est dû. Le portefeuille Telegram, distinct, est non dépositaire.

Limites

  • Les versements et remboursements ne sont pas encore actifs : GET /v1/services renvoie boundaries.livePayouts et boundaries.liveRefunds à false.
  • Les paiements entrants ne sont pas contrôlés par un prestataire KYT.
  • Les factures dont les frais dépasseraient 20 % du montant sont rejetées avec AMOUNT_BELOW_MINIMUM.
  • Aucun SLA contractuel.

Les frais de départ sont de 0,40 % plus les frais de réseau. Demandez un devis de frais pour obtenir le total et le montant minimum ; voir les tarifs.

Authentification

Les appels de serveur à serveur utilisent x-api-key. Les clés de projet sont limitées à un seul projet marchand. Gardez-les sur votre serveur.

Les clés de test commencent par ncp_test_… ; les clés réelles par ncp_live_…. Les clés émises auparavant commencent par ncp_sandbox_ ; elles continuent de fonctionner.

curl -sS "https://api.veilnebu.la/v1/balances" \
  -H "x-api-key: $PROJECT_API_KEY"

Signer les requêtes d’écriture

Les requêtes GET n’ont besoin que de x-api-key. Chaque requête POST, PUT, PATCH et DELETE authentifiée par une clé de projet nécessite aussi deux en-têtes, sinon l’API répond 401 Invalid API signature :

  • x-ncp-timestamp : ISO-8601, à moins de 5 minutes de l’heure actuelle.
  • x-ncp-signature : hex(HMAC-SHA256(your API key, "<timestamp>.<METHOD>.<path incl. query>.<raw body>")).

Écrivez les méthodes en majuscules. Signez exactement les octets que vous envoyez, y compris la query string dans le chemin. Une requête sans corps signe une chaîne vide. Les helpers ci-dessous nécessitent votre clé de projet ; le helper shell nécessite Bash, curl et OpenSSL, et le helper Node.js utilise le fetch natif.

export API_BASE_URL="https://api.veilnebu.la"
export PROJECT_API_KEY="ncp_test_…"

ncp() {
  local method="$1" path="$2" body="" ts sig
  shift 2
  if [ "$#" -gt 0 ]; then body="$1"; shift; fi
  ts=$(date -u +%Y-%m-%dT%H:%M:%S.000Z) || return
  sig=$(printf '%s.%s.%s.%s' "$ts" "$method" "$path" "$body" |
    openssl dgst -sha256 -hmac "$PROJECT_API_KEY" -hex) || return
  sig=${sig##* }
  if [ -n "$body" ]; then
    curl -sS --request "$method" "$API_BASE_URL$path" \
      -H "x-api-key: $PROJECT_API_KEY" \
      -H "x-ncp-timestamp: $ts" -H "x-ncp-signature: $sig" \
      -H "content-type: application/json" --data-binary "$body" "$@"
  else
    curl -sS --request "$method" "$API_BASE_URL$path" \
      -H "x-api-key: $PROJECT_API_KEY" \
      -H "x-ncp-timestamp: $ts" -H "x-ncp-signature: $sig" "$@"
  fi
}

Créer votre première facture

Créez un projet dans le tableau de bord, copiez sa clé API et créez une facture. La réponse contient checkoutUrl : redirigez-y votre client.

ncp POST /v1/payments '{
  "amountMinor":"10000",
  "currencyCode":"USD",
  "settlementAssetCode":"USDT",
  "externalId":"order-1001"
}' -H "idempotency-key: order-1001"

Extrait de réponse. L’ID est généré pour chaque facture.

{
  "id": "cm3k9x2f40001qz8e7h5v1abc",
  "status": "new",
  "amountMinor": "10000",
  "currencyCode": "USD",
  "settlementAssetCode": "USDT",
  "checkoutUrl": "https://app.veilnebu.la/checkout/cm3k9x2f40001qz8e7h5v1abc"
}

amountMinor est une chaîne contenant un entier exprimé dans la plus petite unité de la devise : 10000 = 100.00 USD. Le champ décimal amount n’est accepté que par l’endpoint navigateur /v1/public/payments.

Paiement depuis le navigateur et origines autorisées

Créez une clé publiable dans le tableau de bord et indiquez les origines exactes utilisées par votre boutique. Les requêtes du navigateur l’envoient comme x-publishable-key à POST /v1/public/payments.

L’origine, successUrl et cancelUrl doivent toutes figurer dans la liste des origines autorisées de la clé. Une origine se compose du schéma, de l’hôte et d’un port facultatif, sans chemin ni joker. Les origines absentes ou non listées sont rejetées.

Le montant décimal accepte jusqu’à deux décimales pour les devises fiat, et jusqu’au nombre de décimales propre à l’actif pour les cryptos (USDT et USDC : 6 sur tous les réseaux que nous prenons en charge aujourd’hui).

const response = await fetch('https://api.veilnebu.la/v1/public/payments', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'x-publishable-key': 'pk_test_…',
  },
  body: JSON.stringify({
    amount: '100.00',
    currencyCode: 'USD',
    settlementAssetCode: 'USDT',
    successUrl: 'https://shop.example/thanks',
    cancelUrl: 'https://shop.example/cart',
  }),
});
if (!response.ok) throw new Error(await response.text());
const invoice = await response.json();
window.location.assign(invoice.checkoutUrl);

Statuts de paiement

StatutAction suivante
newPartager le lien de paiement
pendingEn attente du paiement
confirmingEn attente des confirmations
paidPrêt pour le règlement
partially_paidDemander au client de compléter le paiement
overpaidVérifier le montant excédentaire crédité
expiredCréer une nouvelle facture
cancelledAucune action requise
failedOuvrir les détails
refundedAucune action requise

Clés d’idempotence

Pour créer une facture, envoyez une Idempotency-Key stable dérivée de l’ID de votre commande. Une nouvelle tentative avec la même clé et le même corps renvoie la facture d’origine. La réutiliser avec un corps différent renvoie 409. Gardez la clé et le corps inchangés lorsque vous réessayez après un timeout.

Voir l’exemple de première facture pour la requête.

Erreurs

Les erreurs sont au format JSON. message est une chaîne, ou un tableau de chaînes pour les erreurs de validation.

{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid API key"
}
Statut HTTPSignification et action
400Champ invalide ; "message" indique le champ. Corrigez-le avant de réessayer.
401Invalid API key ou Invalid API signature. Vérifiez la clé, les octets signés et l’horodatage.
404Payment invoice not found. Vérifiez l’ID et le projet.
409Même Idempotency-Key avec un corps différent, ou Insufficient merchant payable balance.
429Limite de débit atteinte. Patientez avant de réessayer.
5xxRéessayez avec la même Idempotency-Key et le même corps.

Webhooks (HMAC-SHA256)

Chaque livraison contient x-nebula-signature: t=<unix>,v1=<hex>. Vérifiez le HMAC avec le secret de l’endpoint renvoyé lors de sa création. Rejetez les horodatages éloignés de plus de 300 secondes de l’heure actuelle.

Vérifier en Node.js

Le vérificateur Node.js utilise uniquement le module crypto intégré. Passez-lui l’en-tête de signature, les octets bruts du corps et le secret de l’endpoint.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(header, rawBody, secret) {
  const parts = {};
  for (const piece of (header ?? '').split(',')) {
    const separator = piece.indexOf('=');
    if (separator <= 0) continue;
    parts[piece.slice(0, separator).trim()] = piece.slice(separator + 1).trim();
  }
  if (!/^[0-9]{1,16}$/.test(parts.t ?? '')) return false;
  if (!/^[0-9a-f]{64}$/.test(parts.v1 ?? '')) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(parts.t + '.')
    .update(rawBody)
    .digest('hex');
  if (expected.length !== parts.v1.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Vérifiez sur le corps brut de la requête, avant le parsing JSON. La livraison a lieu au moins une fois : dédupliquez sur x-nebula-event-id, qui correspond à eventId dans le payload. Les nouvelles tentatives automatiques conservent l’ID de livraison ; un renvoi manuel crée un nouvel ID de livraison avec le même ID d’événement. Enregistrez l’événement avant de répondre 2xx.

Tester les webhooks en local

Les URL de webhook doivent utiliser un hôte public sur Internet. Les adresses locales et privées sont rejetées. Exposez votre récepteur via un tunnel HTTPS public, puis enregistrez son URL avec le helper de signature.

ncp POST /v1/webhooks/endpoints '{
  "url":"https://receiver.example/webhook",
  "events":["payment.paid","payment.reorged"]
}'

Remplacez l’URL par celle de votre récepteur. Conservez le secret de l’endpoint renvoyé à la création.

Types d’événements

Les versements et remboursements ne sont pas encore actifs (GET /v1/services renvoie boundaries.livePayouts à false). D’ici là, les versements et lots de versements ne font qu’enregistrer le transfert dans votre solde ; payout.completed et refund.completed ne signifient pas que des fonds sont sortis.

Le catalogue ci-dessous correspond à GET /v1/services. Une entrée du catalogue ne signifie pas que le mouvement de fonds correspondant est actif.

ÉvénementSignification
webhook.testLivraison de test demandée pour un endpoint de webhook.
payment.pendingLe client a choisi un moyen de paiement ; en attente du premier dépôt.
payment.confirmingDépôt détecté ; en attente des confirmations requises.
payment.paidFacture entièrement payée et réglée dans le grand livre.
payment.partially_paidMontant reçu inférieur à celui demandé ; en attente du reste.
payment.overpaidMontant reçu supérieur à celui demandé ; réglé dans le grand livre avec le trop-perçu.
payment.expiredFacture expirée sans fonds suffisants.
payment.cancelledFacture annulée avant paiement.
payment.failedÉchec du paiement dû à une erreur de chaîne, de politique de frais ou d’observation.
payment.risk_flaggedÉvénement de risque informatif concernant un dépôt déjà crédité. Le contrôle KYT des paiements entrants n’est pas actif.
payment.reorgedUn dépôt confirmé a été annulé par une réorganisation de la chaîne ; la facture n’est plus payée, suspendez ou annulez donc l’exécution de la commande.
payment.unsupported_assetUn jeton non pris en charge a été déposé ; il n’a pas été crédité.
payout.processingVersement en cours de traitement. Les transferts réels ne sont pas ouverts.
payout.completedVersement enregistré dans le solde ; cela ne signifie pas que des fonds sont sortis.
payout.failedÉchec du versement.
payout.cancelledVersement annulé avant son achèvement.
refund.processingRemboursement en cours de traitement. Les remboursements réels ne sont pas ouverts.
refund.completedRemboursement enregistré dans le solde ; cela ne signifie pas que des fonds sont sortis.
refund.failedÉchec du remboursement.
internal_transfer.sentTransfert de solde entre marchands validé pour l’expéditeur.
internal_transfer.receivedTransfert de solde entre marchands crédité au destinataire.
static_wallet.createdUne adresse de dépôt permanente a été créée pour un client.
static_wallet.blockedUn portefeuille statique a été bloqué.
static_wallet.unblockedLe blocage d’un portefeuille statique a été levé.
static_wallet.topup.completedUn dépôt sur un portefeuille statique a été détecté et réglé dans le grand livre.
dispute.openedUn litige a été ouvert sur une facture.
dispute.resolvedUn litige a été résolu. Une résolution seule ne prouve pas un remboursement on-chain.
dispute.rejectedUn litige a été rejeté ; aucune action du marchand n’est requise.
subscription.invoice_createdUn cycle de facturation a créé une nouvelle facture.

Politique de nouvelles tentatives

Si votre endpoint ne répond pas 2xx, nous effectuons 8 tentatives au total : la tentative initiale, puis de nouvelles tentatives après 1 minute, 5 minutes, 30 minutes, 2 heures, 6 heures, 12 heures et 24 heures. Chaque délai commence après la tentative échouée précédente. Les erreurs réseau et les timeouts comptent aussi comme des échecs. Les livraisons de test n’ont qu’une seule tentative.

Après le dernier échec, la livraison est marquée comme échouée ; renvoyez-la avec POST /v1/webhooks/deliveries/{deliveryId}/resend. En secours, effectuez le rapprochement avec GET /v1/payments/{id}.

Les versements et remboursements ne sont pas encore actifs (GET /v1/services renvoie boundaries.livePayouts à false). D’ici là, les versements et lots de versements ne font qu’enregistrer le transfert dans votre solde ; payout.completed et refund.completed ne signifient pas que des fonds sont sortis.

Lots de versements

Un lot regroupe des enregistrements de transferts. Sa politique d’approbation et le nombre d’approbations requises sont renvoyés dans approvalPolicy et approvalsRequired. S’il y a plusieurs approbateurs, le créateur ne peut pas approuver son propre lot. Un opérateur seul habilité peut approuver un lot créé dans sa propre session. Approuvez les lots avec WebAuthn dans le tableau de bord.

export PROJECT_API_KEY="ncp_test_…"
ncp POST /v1/payout-batches '{
  "assetCode":"USDT",
  "networkCode":"TRON",
  "externalId":"batch-1001",
  "items":[{
    "destinationAddress":"T…",
    "amountMinor":"15000000",
    "externalRef":"recipient-1001"
  }]
}' -H "idempotency-key: batch-1001"

Remplacez la destination par une adresse valide dans votre propre projet de test.

Facturation récurrente

Un abonnement crée une nouvelle facture à chaque cycle de facturation et la signale avec subscription.invoice_created. Il ne débite pas automatiquement le portefeuille du client. Envoyez au client la nouvelle URL de la page de paiement.

interval : DAY, WEEK, MONTH ou YEAR.

ncp POST /v1/subscriptions '{
  "customerRef":"customer-42",
  "assetCode":"USDT",
  "networkCode":"TRON",
  "amountMinor":"9000000",
  "interval":"MONTH",
  "intervalCount":1
}'

Mettez en pause, reprenez ou annulez avec les opérations de cycle de vie décrites dans la référence de l’API.

Contrôle des risques

Les destinations des versements et remboursements sont comparées aux listes de sanctions publiques avant la création du transfert ; une correspondance entraîne le rejet de la requête. Les paiements entrants ne sont pas contrôlés.

Politique LCB-FT et sanctions

Bot Telegram pour marchands

@veilnebula_bot fonctionne avec un seul projet à la fois. Associez-le dans une conversation privée avec la clé API de ce projet. Le bot tente de supprimer votre message et chiffre la clé stockée. Si la suppression échoue, révoquez la clé et remplacez-la. Ne publiez jamais une clé dans une conversation partagée.

Les notifications concernent le projet associé et la clé active. Ouvrez le tableau de bord pour approuver les versements.

/link <API_KEY>
/balance
/invoices
/notify on
/unlink
Guide d’intégration · VeilNebula