Webhooks : événements, signatures et livraison
Enregistrez un point de terminaison HTTPS public, prouvez-en le contrôle et recevez des événements de facture et de paiement signés. La livraison est mise en file de manière durable ; traiter le même événement à nouveau doit être sans danger pour votre commande.
Sur cette page
D’où viennent les événements#
invoice.created ne signifie pas payé. Les événements de paiement proviennent d’une opération serveur vérifiée après une lecture actuelle de la passerelle ; un nom d’événement entrant seul est insuffisant.
L’entrée Passerelle → Kevmi et la livraison Kevmi → marchand sont des tronçons distincts avec des secrets séparés. Le navigateur, la navigation de retour et les messages du widget ne définissent pas l’état financier.
| Événement | Source |
|---|---|
| invoice.created | Facture enregistrée |
| invoice.updated | Disponibilité du lien modifiée |
| payment.succeeded | Paiement exact vérifié par le serveur |
| payment.failed | Opération échouée vérifiée |
| payment.refunded | Remboursement complet vérifié |
| payment.partially_refunded | Remboursement partiel vérifié |
| payment.disputed | Litige vérifié |
| payment.review_required | L’opération nécessite une révision supplémentaire |
Enregistrer et vérifier un point de terminaison#
Les URL ne peuvent pas cibler des adresses IP, localhost, des réseaux privés, des ports non standard ni contenir d’identifiants. Le DNS est vérifié avant la livraison ; les requêtes POST sortantes ne suivent pas les redirections. Un compte peut enregistrer jusqu’à cinq points de terminaison.
L’événement de vérification a livemode=false. Un statut HTTP réussi seul est insuffisant : le défi doit correspondre exactement. La rotation du secret nécessite une nouvelle vérification.
- 1
Sur /account#webhooks dans votre compte marchand, saisissez une URL HTTPS publique, une description et les types d’événements dont vous avez besoin.
- 2
Enregistrez le secret émis dans le stockage côté serveur. Sa valeur complète n’est affichée qu’une fois.
- 3
Vérifiez les signatures sur les octets de la requête d’origine.
- 4
Exécutez la vérification du point de terminaison. Pour endpoint.verification, renvoyez un JSON reflétant data.challenge : {"challenge":"la valeur reçue"}.
- 5
Activez le point de terminaison vérifié et inspectez l’historique de livraison.
Enveloppe d'événement#
id reste identique à chaque tentative de livraison d'un événement. createdAt est un horodatage Unix en millisecondes, et apiVersion identifie la version du contrat. data contient l'objet serveur persisté ; l'exemple est raccourci.
livemode=true distingue un véritable événement serveur de la vérification d'endpoint. Cela ne signifie pas qu'une facture est payée : vérifiez le type et les données de paiement. Gérez les types d'événements connus et validez le schéma.
{
"id": "evt_example",
"type": "invoice.created",
"apiVersion": "2026-10-21",
"createdAt": 1792540800000,
"livemode": true,
"data": {
"invoice": {
"id": "lnk_example",
"orderId": "order-1042",
"currency": "USD",
"amountMinor": 500,
"vatCustomerPercent": 100,
"feeCustomerPercent": 0
}
}
}Vérifier HMAC-SHA256#
Kevmi-Signature contient t=secondes,v1=hex. La signature est HMAC-SHA256 sur la chaîne d'horodatage, un point et le corps d'origine : horodatage + "." + rawBody. Utilisez le secret d'endpoint spécifique et une comparaison à temps constant.
Rejetez les signatures dont l'horodatage diffère de l'heure du serveur de plus de 300 secondes dans un sens ou dans l'autre. Synchronisez les horloges. N'analysez pas et ne resérialisez pas le JSON avant la vérification : les espaces et l'ordre des propriétés affectent les octets signés.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyKevmi(rawBody, header, secret, nowSeconds) {
const match = /^t=(\d{10}),v1=([a-f0-9]{64})$/.exec(header || '');
if (!match || Math.abs(nowSeconds - Number(match[1])) > 300) {
return false;
}
const expected = createHmac('sha256', secret)
.update(match[1] + '.')
.update(rawBody)
.digest();
const received = Buffer.from(match[2], 'hex');
return expected.length === received.length
&& timingSafeEqual(expected, received);
}
// Read bytes before JSON parsing; cap the request body.
// secret comes from your server secret store.
const valid = verifyKevmi(rawBody, signature, secret,
Math.floor(Date.now() / 1000));
if (!valid) throw new Error('Invalid webhook signature');
const event = JSON.parse(rawBody.toString('utf8'));
// Validate the event schema and persist its ID together
// with your order update in one database transaction.
// Return a 2xx response after durable acceptance.Nouvelles tentatives et accusé de réception#
Toute réponse 2xx accuse réception d'un événement ordinaire. Persistez l'événement de manière durable avant de répondre ; le travail coûteux peut se poursuivre via votre propre file d'attente. Le délai de réponse est de huit secondes. Une erreur réseau ou une réponse non-2xx planifie une nouvelle tentative.
Il y a au plus six tentatives : la livraison initiale plus des délais d'une minute, cinq minutes, 30 minutes, deux heures et 12 heures après des échecs successifs. Ce sont des plannings de file d'attente, pas des garanties d'heure de livraison exacte. Épuiser les tentatives marque la livraison comme échouée.
La livraison est au moins une fois et ne promet pas un ordre strict. Persistez l'ID d'événement unique dans la même transaction que votre mise à jour de commande. Un doublon déjà appliqué doit recevoir 2xx sans exécuter à nouveau la commande.
Une nouvelle tentative manuelle redémarre les tentatives de livraison tout en préservant l'ID d'événement d'origine, l'ID de livraison et le corps exact. Chaque tentative reçoit un horodatage de signature frais. Suspendre un endpoint préserve sa file d'attente ; le supprimer annule les livraisons en attente et échouées.
Historique de livraison et rapprochement#
L'interface affiche l'état de l'endpoint, les livraisons récentes, les nombres de tentatives, la dernière réponse HTTP, l'erreur et l'heure de la prochaine tentative. Ces diagnostics complètent vos propres enregistrements de commandes.
GET /api/v1/events avec events:read renvoie jusqu'à 50 événements et nextCursor. Passez nextCursor opaque inchangé en tant que cursor ; null marque la fin. L'ordre utilise à la fois createdAt et id. L'ancienne limite before Unix-milliseconde reste disponible séparément et ne peut pas être combinée avec cursor. Les endpoints vérifiés reçoivent les événements souscrits au plus tôt à leur enregistrement ; les endpoints suspendus continuent d'accumuler les événements en file d'attente pour livraison après reprise. Cela ne promet pas d'historique antérieur à la création de l'endpoint.
Vérifications des reçus de passerelle entrants#
Gateway v3 publie sur /api/gateway/webhook avec X-Gateway-Event-Id, X-Gateway-Timestamp et X-Gateway-Signature : v1=<hex>. HMAC-SHA256 signe horodatage + "." + rawBody avec GATEWAY_WEBHOOK_SECRET ; la fenêtre d'horloge est de 300 secondes. L'égalité de l'ID en-tête/corps, la version API, le mode live et le hachage du corps sont vérifiés.
Un ID d'événement réutilisé avec des octets de corps différents est rejeté. Une notification signée déclenche un GET authentifié de l'opération exacte. Ordre, méthode, devise, base, total approuvé et version sont vérifiés. Le succès exige fulfillment_ready=true, verification=provider_confirmed, le montant payé exact et aucun litige ; la crypto exige également une preuve de règlement confirmée.
Le traitement entrant, les reçus et les événements marchands sont protégés contre les doublons. Les anciens formats de notification uniquement navigateur ne peuvent pas établir un paiement réussi. Les webhooks marchands ne contiennent jamais de champs de carte, codes bancaires, données QR, identifiants de fournisseur ou le secret de passerelle.
Telegram a un contrat séparé#
POST /api/telegram/webhook vérifie X-Telegram-Bot-Api-Secret-Token, la structure de mise à jour et l'expéditeur. update_id protège contre le traitement en double. Cette route gère la connexion, les commandes, le support et les actions de modération ; elle n'accepte pas les reçus financiers.
Le propriétaire du déploiement configure le jeton du bot, le secret du webhook, l'enregistrement setWebhook et l'adresse HTTPS accessible. Ils sont séparés du secret de signature de l'endpoint marchand.
Plusieurs boutiques et un prix de base USD unique#
Choisissez Toutes les boutiques pour les analyses et opérations agrégées, ou sélectionnez une boutique pour ses propres enregistrements. Chaque boutique possède un storeId, un nom, une TVA par défaut, des factures, des clés API, des points de terminaison webhook, un historique d'événements et une application de site Web distincts. Les notifications et les tickets de support appartiennent au compte. Un compte peut créer jusqu'à 100 boutiques. Les enregistrements existants restent dans sa première boutique.
Les factures du nouvel espace de travail utilisent pricingMode="universal" et un prix de base en USD. Any inclut carte, alipay, sbp et crypto ; un commerçant peut sélectionner un sous-ensemble. Les cartes et Alipay facturent en USD. SBP facture en RUB en utilisant le taux de référence USD/RUB de la Banque de Russie. La crypto facture en USDT selon la règle de facturation explicite 1 USD = 1 USDT. Il s'agit d'une règle de conversion de facturation, pas d'une cotation USDT de marché. La passerelle fournit le taux de la pièce sélectionnée, le montant exact du transfert et les instructions avant le transfert.
Le taux de référence et les deux montants sont gelés à la création. Les factures impliquant une conversion RUB expirent dans les 24 heures. Si le taux de référence ne peut pas être obtenu, la création échoue avec FX_UNAVAILABLE ; aucun taux de secours inventé ou obsolète n'est utilisé. Le montant en monnaie mineure du propriétaire et les cotations utilisent la monnaie comptable USD. quotes[method].settlement contient la cotation de frais native. Le paiement public expose la monnaie de frais sélectionnée dans currency, le prix d'origine dans baseAmountMinor/baseCurrency, et le taux de référence dans fx. Une fois qu'un paiement a été envoyé, sa méthode ne peut pas être modifiée ; une facture a une seule opération de passerelle.
Créez une clé API dans la boutique prévue et envoyez Authorization: Bearer <key> depuis votre serveur. Sa liaison de boutique est immuable ; X-Kevmi-Store ne peut pas lui accorder l'accès à une autre boutique. Les sessions de navigateur sélectionnent X-Kevmi-Store: <storeId> ; si omis, la première boutique est sélectionnée. La valeur all n'est acceptée que par GET /api/account/dashboard et GET /api/payment-links. Les mutations nécessitent une boutique sélectionnée, Origin et X-CSRF-Token. Les identifiants de compte et les identifiants de boutique sont des identifiants différents.
Pour la compatibilité, les demandes API sans pricingMode="universal" conservent la tarification native et les restrictions de monnaie d'origine. Les factures existantes ne sont jamais recalculées. Les reçus confirmés dans les analyses reflètent les paiements vérifiés moins les dépenses de facture enregistrées ; ils ne constituent pas un solde retirable. Les monnaies de facture héritées restent séparées et les paiements sont organisés avec le support. Exécutez une commande uniquement après avoir vérifié payment.fulfillmentReady=true et payment.status="succeeded" via l'API serveur, l'historique d'événements ou un webhook signé.