API

Intégration Drop-in du paiement en ligne

Intégration du widget de carte

Le widget de carte Yavin vous permet d'encaisser des paiements par carte sur votre propre page de paiement. Le numéro de carte, la date d'expiration, le co-badge et le CVC sont saisis dans une iframe appartenant à Yavin et n'atteignent jamais votre page ni vos serveurs : vous conservez ainsi le design de votre page de paiement tout en restant dans le périmètre PCI le plus léger.

Périmètre de la v1 : paiements par carte bancaire uniquement. Les cartes titres-restaurant, les portefeuilles Swile/Edenred et Apple Pay ne sont pas pris en charge. Le widget affiche les champs de carte ; vous gérez le bouton de paiement, l'interface de pourboire et tout ce qui les entoure.

Le runtime hébergé n'est pas encore disponible. https://pay.yav.in/sdk/ et https://pay.s.yav.in/sdk/ ne répondent pas à la date de rédaction. Tout ce qui suit décrit la forme à laquelle vous intégrer : confirmez l'hôte auprès de nous avant de déployer.


Fonctionnement

Quatre participants et une frontière de confiance (un cinquième, la page 3-D Secure de la banque, n'intervient que lors d'une authentification renforcée). Votre backend obtient un token de paiement auprès de l'API Yavin et le transmet à votre propre frontend ; votre page l'utilise ensuite pour monter le widget. Le numéro de carte et le CVC n'existent qu'à l'intérieur de l'iframe Yavin : votre page reçoit la validité des champs et le résultat du paiement, jamais les données de carte.

Le parcours sans friction

La plupart des paiements aboutissent sans que le porteur de carte soit soumis à une authentification : votre page monte le widget et submit() se résout dans la page avec le résultat.

Le token de paiement est émis par l'API Yavin à destination de votre backend, jamais par le widget. Le widget ne le reçoit que de votre page.

L'authentification renforcée 3-D Secure

Lorsque l'émetteur exige une authentification, la page de la banque ne peut pas être affichée dans une iframe : le widget redirige donc tout l'onglet. submit() ne se résout jamais ; le client revient via les URL de retour du paiement.

Pourquoi une iframe. Le champ de carte est servi depuis l'origine propre à Yavin, si bien que la politique de même origine du navigateur empêche les scripts de votre page, ainsi que tout tag tiers qu'elle contient, de lire la carte. C'est ce qui vous maintient éligible à l'auto-évaluation PCI la plus légère : vous ne voyez, ne stockez et ne transmettez jamais de numéros de carte.

Le widget gère la saisie et le formatage de la carte, la détection du réseau de carte, la sélection du co-badge, la requête de paiement, le 3-D Secure et le suivi des nouvelles tentatives. Il effectue lui-même ses appels réseau ; vous n'avez jamais à les construire.


Étape 1 : créer un paiement

Un paiement associe une transaction à un montant et à ses URL de retour. Créez-le côté serveur avec votre clé API, jamais dans le navigateur, puis transmettez le token obtenu à votre page.

Les paramètres sont exactement ceux d'un lien de paiement (checkout_external_id, amount, items, customer, features et les autres) ; ils ne sont donc pas répétés ici. Consultez Paiement en ligne : E-commerce API pour la liste complète.

checkout_token peut être exposé sans risque au navigateur : il est limité à ce seul paiement. payment_link est le lien court du paiement.

Ce qui diffère pour le widget

ChampSensRemarques
checkout_tokenréponseLa valeur que vous transmettez au navigateur et que vous passez à mount() sous la forme checkoutToken. C'est la même valeur, aux conventions de nommage près : snake_case dans les échanges réseau, camelCase en JavaScript.
payment_linkréponseLe lien court du checkout. Avant le paiement, il ouvre le checkout hébergé de Yavin ; une fois le checkout payé, la même URL redirige vers le ticket numérique, ce qui en fait un lien pratique « voir votre ticket de carte » sur votre page de confirmation.
amountrequêteEntier, en unités mineures. Minimum 100 (1,00 €) — un montant inférieur est rejeté par la validation.
return_url_successrequêteURL vers laquelle le client est renvoyé après une authentification 3-D Secure réussie. Facultatif, mais si vous l'omettez, le client arrive sur le lien court du checkout et non sur votre site.

Conservez la clé secrète côté serveur. Elle permet d'ouvrir des checkouts et de déplacer des fonds. Le navigateur ne reçoit jamais que le token du checkout, limité à ce seul checkout. Ne l'exposez jamais, en particulier, via une variable de bundler injectée dans le JavaScript client — par exemple une variable d'environnement préfixée par NEXT_PUBLIC_ dans Next.js.

Les champs des requêtes et des réponses sont en snake_case. Les options JavaScript du widget suivent la convention JS (checkoutToken) — passez-lui directement le checkout_token que vous avez reçu.

Le token du checkout reste valide après des tentatives échouées : un client peut donc réessayer sur le même checkout. Il devient inutilisable dès que le checkout est réglé ou annulé, ce que le widget signale par checkout_already_paid ou session_unusable.

Un checkout_external_id est réservé définitivement. La vérification des doublons ignore le statut du checkout : un checkout annulé conserve donc son identifiant externe pour toujours, et le réutiliser renvoie 409. Si vous remplacez un checkout après que le client a modifié sa commande, donnez au nouveau checkout un identifiant externe distinct — le dériver du montant est un moyen simple d'y parvenir.


cURL
POST https://api.yavin.com/api/v5/ecommerce/checkouts/
Yavin-Secret: <your yavin_secret_key>
Content-Type: application/json

{ "checkout_external_id": "order_1234",
  "amount": 2900,
  "items": [
    { "name": "Sneakers", "quantity": 1, "total_amount": 2900 }
  ],
  "return_url_success":   "https://shop.example.com/return",
  "return_url_cancelled": "https://shop.example.com/cancel",
  "webhook_url":          "https://shop.example.com/hooks/yavin" }
JSON
// 201 Created
{ "checkout_token": "Ab3f...",
  "payment_link": "https://..." }

Étape 2 - Monter le champ carte

Le runtime enregistre window.YavinPaymentsSDK lors de son évaluation : une simple balise script suffit donc pour une intégration complète. mount(element, options) affiche le champ dans votre conteneur et renvoie un handle de widget. Le champ s'ajuste automatiquement à son contenu — vous n'avez jamais à gérer la hauteur de l'iframe.

Si vous chargez les scripts depuis un framework plutôt qu'en écrivant le balisage à la main — Next.js, Vue ou n'importe quel routeur de SPA — injectez la balise vous-même. C'est strictement équivalent au balisage, et il est essentiel que ce soit un élément script classique, et non un module :

Deux règles de chargement faciles à manquer.

  • type="module" n'est pas pris en charge : un script de type module est récupéré en mode CORS et document.currentScript y vaut null, alors que c'est ce qui permet au runtime de déterminer de quel environnement il provient, et donc où se trouve son iframe de carte
  • Le gestionnaire onerror n'est pas décoratif : une balise seule n'a pas de délai d'expiration, ne vérifie jamais que l'objet global a bien été enregistré et ne peut pas réessayer ; si la récupération est bloquée, YavinPaymentsSDK reste indéfini et votre ligne suivante lève une ReferenceError

Chargez le script depuis l'environnement auquel appartient votre checkout. Le runtime déduit l'origine de son iframe de l'URL de son propre script : un token créé en sandbox ne fonctionnera donc pas avec un runtime servi depuis l'hôte de production.

OptionTypeRemarques
checkoutTokenstringObligatoire. Le checkout_token provenant de votre backend. mount() lève une exception de façon synchrone s'il est absent ou vide.
localestringLangue du champ : "en", "fr" ou "es". Par défaut, la langue du navigateur si elle fait partie de celles-ci, sinon "fr". Toute autre valeur lève une exception.
themeThèmeTokens de couleur et de police — voir Personnalisation et localisation.
labelsLibellésRemplacements partiels des libellés, des placeholders et des messages d'erreur. Les champs omis conservent le texte d'origine du widget pour la langue sélectionnée.
layoutstring"fields" — la seule disposition implémentée. Omettez-la, sauf si vous souhaitez être explicite.
returnUrlErrorstringURL vers laquelle le client est renvoyé après un échec de l'authentification 3-D Secure. Par défaut, l'URL de cette page sans chaîne de requête ni fragment, si bien que la plupart des intégrations ne la définissent jamais. Son origine doit être enregistrée pour votre compte.

Appelez widget.destroy() pour démonter le champ, par exemple lorsque le client quitte votre étape de paiement. Monter le champ dans un conteneur qui en contient déjà un retire d'abord le précédent : un nouveau montage est donc sans risque.


Node
<script src="https://pay.yav.in/sdk/v1/yavin.js" onerror="paymentsUnavailable()"></script>
<div id="card"></div>
<button id="pay" disabled>Pay 29.00</button>
Node
const tag = document.createElement('script')
tag.src = 'https://pay.yav.in/sdk/v1/yavin.js'
tag.onerror = () => paymentsUnavailable()
tag.onload = () => mountTheField()
document.head.appendChild(tag)
Node
const widget = window.YavinPaymentsSDK.mount(
  document.getElementById('card'),      // an element, not a selector string
  { checkoutToken: 'Ab3f...', locale: 'fr' }
)

Étape 3 - Interagir avec le widget

Abonnez-vous avec widget.on(event, handler) et désabonnez-vous avec widget.off. Les événements ne transportent que des états non sensibles — de quoi piloter votre bouton et refléter la carte détectée — jamais de données de carte. Les abonnements appartiennent au montage qui les a créés et sont supprimés par destroy() : il n'y a donc rien à désabonner manuellement.

ÉvénementPayloadUtilisation
ready{}Savoir que le champ a accepté le handshake et qu'il est utilisable. Il ne se déclenche jamais pour un handshake refusé par le champ, ce qui en fait le seul signal de disponibilité fiable.
validity{ fields: { number, expiry, cvc }, allValid }Activer votre bouton de paiement sur allValid
scheme{ scheme }Refléter le choix de co-badge du porteur de la carte — "cb", "visa" ou "mastercard"
resultPaymentResultObserver le résultat d'une tentative depuis du code qui n'a pas appelé submit() — pour de l'analytique, par exemple. Il porte la même valeur que celle avec laquelle la promesse est résolue.
threeDsChallenge{}Se déclenche juste avant que l'onglet ne redirige vers la banque. Utilisez-le pour conserver l'état dont vous avez besoin au retour. Ce n'est pas un résultat.
errorCheckoutErrorRemonter toute erreur signalée par le champ, pendant ou en dehors d'une tentative — y compris les problèmes au démarrage qu'aucun submit() ne verrait jamais.

Le co-badge est un choix du porteur de la carte. Lorsqu'une carte porte plusieurs réseaux, le widget affiche un sélecteur et le porteur choisit — la réglementation européenne lui réserve ce choix, jamais au marchand. Votre page se contente de le refléter via l'événement scheme.

Conditionnez votre bouton de paiement à ready et allValid. Le retour de mount() signifie seulement que le chargement de la frame a démarré ; la négociation peut encore échouer ensuite, et une origine non enregistrée n'apparaît qu'environ 20 secondes plus tard, sous la forme d'une erreur handshake_timeout.


Node
widget.on('ready', () => console.log('field usable'))
widget.on('validity', ({ allValid }) => pay.disabled = !allValid)
widget.on('error', ({ code, message, retriable }) => show(message))

Étape 4 : encaisser le paiement

widget.submit(options) indique au widget d'encaisser le paiement avec la carte saisie. Transmettez giftAmount en centimes pour ajouter un pourboire choisi dans votre propre interface.

submit() ne se résout pas lors d'une authentification renforcée 3-D Secure. Le widget redirige tout l'onglet vers la banque et votre page est déchargée : le await ci-dessus ne retourne donc jamais. Le résultat arrive alors via vos URL de retour et vos webhooks. N'accédez à switch que lorsqu'aucune authentification renforcée n'était nécessaire.

statusChampsSignification et action à mener
succeededtransactionId?Paiement signalé comme réussi. Affichez votre confirmation, mais confirmez côté serveur avant de livrer la commande.
partially_paidpaidAmount, amountLeft, transactionId?Il s'agit d'un succès, pas d'une erreur : la tentative a encaissé paidAmount, mais il reste amountLeft à régler sur le paiement, car une partie avait déjà été payée par un autre moyen. Encaissez le solde avant de considérer la commande comme terminée.
failederror, transactionId?Un CheckoutError. Affichez votre propre message à partir de error.code et ne proposez une nouvelle tentative que si error.retriable vaut true.

transactionId est la valeur que les webhooks payment_received / payment_failed publient sous transaction_id : le résultat obtenu dans le navigateur peut ainsi être rattaché à l'événement serveur qui valide la commande. Ce champ est facultatif : le backend l'omet lorsque la réponse ne désigne aucune tentative qui vous appartienne.

submit() n'est rejetée que lorsqu'il n'y a aucun résultat : le champ a été détruit pendant la tentative. Un refus est un résultat et se résout en failed. Appeler submit() à nouveau pendant qu'une tentative est en cours renvoie la promesse de cette même tentative au lieu de lancer une seconde autorisation.

Tous les montants sont des entiers exprimés dans la plus petite unité monétaire (centimes).

Ne considérez jamais le résultat du navigateur comme une preuve de paiement. C'est un simple signal d'expérience utilisateur. Confirmez l'état réglé côté serveur à partir de votre webhook, ou en interrogeant le paiement, avant de livrer les biens.


Node
pay.onclick = async () => {
  // On a 3-D Secure card this never returns; the widget takes over the tab and
  // the customer comes back via your return URLs (see 3-D Secure).
  const result = await widget.submit()        // optional: { giftAmount: 150 }
  switch (result.status) {
    case 'succeeded':
      showConfirmation(result.transactionId)
      break
    case 'partially_paid':
      collectRest(result.paidAmount, result.amountLeft)
      break
    case 'failed':
      show(result.error.message)              // result.error.retriable says whether to offer a retry
      break
  }
}

3-D Secure

Lorsque l'émetteur exige une authentification renforcée, la page d'authentification de la banque ne peut pas s'afficher dans une frame : le widget soumet donc un formulaire POST de niveau supérieur et tout l'onglet y est redirigé. Votre page est déchargée et submit() ne se résout jamais. Le client revient de la façon suivante :

RésultatPage d'arrivéeDonnées transmises
Authentifié et payéreturn_url_success, ou le lien court du paiement si vous n'en avez pas définiParamètres de requête cartId et status
ÉchecreturnUrlError : par défaut, la page sur laquelle le client a lancé le paiementpayment_error : refused, gateway_error ou error

Le retour en cas d'échec ne nécessite aucune configuration. Le widget s'exécute sur votre page : il lit donc son adresse et l'envoie par défaut comme URL de retour, c'est-à-dire la page sur laquelle le client a commencé, sans sa query string ni son fragment.

  • Transmettez mount({ returnUrlError }) uniquement pour rediriger le client ailleurs
  • Dans les deux cas, l'origine doit être enregistrée pour votre compte, exactement comme l'origine depuis laquelle vous intégrez le widget ; une origine non enregistrée est refusée avant toute tentative de paiement et signalée par origin_not_allowed

Lors du retour en cas de succès, le client s'est authentifié : affichez votre étape de confirmation, mais considérez le paramètre status comme une simple indication et confirmez le paiement à partir du webhook ou d'une interrogation du paiement avant de livrer la commande.

Une nouvelle tentative ne nécessite aucun état de votre part. Le retour est un chargement complet de page : votre page remonte donc le widget sur le jeton de paiement toujours valide, et un nouvel appel à submit() démarre une nouvelle tentative. L'identité de la tentative est gérée côté serveur ; vous n'avez pas à la gérer.


Gestion des erreurs

Le résultat d'un paiement, réussi ou non, vous parvient par trois canaux : le résultat submit() dans la page, les paramètres de l'URL de retour et le webhook. Les deux canaux du navigateur fournissent un retour immédiat pour piloter votre interface, tandis que le webhook (ou une interrogation du paiement) constitue votre confirmation de référence.

CanalMoment de déclenchementContenu et niveau de confiance à lui accorder
La promesse submit() et l'événement resultUniquement pour les paiements sans friction, jamais après une authentification renforcée 3-D Secure ayant redirigé l'ongletUn PaymentResult. Une indication immédiate pour l'expérience utilisateur ; elle est tout simplement absente dès que la page a été déchargée pour 3-D Secure.
Le paramètre payment_error lors du retour 3-D SecureAprès l'échec d'une authentification renforcée 3-D Securerefused, gateway_error ou error, sans message. Moins précis que la promesse, visible par le client et trivialement falsifiable — à n'utiliser que pour l'affichage.
Le webhook payment_failed / payment_receivedÀ chaque tentative, de serveur à serveurL'échec reason et le checkout status. La source de vérité — mais asynchrone, soumise à des nouvelles tentatives, et potentiellement jamais livrée si votre endpoint est inaccessible.

Les deux premiers sont le même événement vu depuis le navigateur : vous obtenez la promesse submit() ou les paramètres de retour, jamais les deux — la seule différence est de savoir si 3-D Secure a déchargé la page. Le webhook est une vue distincte et faisant autorité, et il peut contredire ce que le navigateur a affiché.

Les trois canaux ne partagent pas le même vocabulaire. Le error.code ci-dessous, la catégorie payment_error et le reason du webhook sont trois tables distinctes qui ont simplement certains libellés en commun. Ne résolvez jamais la valeur d'un canal à l'aide de la table d'un autre.

Codes d'erreur

Chaque échec signalé par le widget — sur l'événement error ou sous forme de result.error — est un CheckoutError :

codecatégorienouvelle tentative possibleSignification
payment_refusedcarteouiRefusé par l'émetteur. Invitez le client à réessayer avec une autre carte.
insufficient_fundscarteouiLa carte ne dispose pas de fonds suffisants.
authorisation_failedpasserelleouiLe paiement n'a pas pu être autorisé.
three_ds_failedauthentificationouiL'authentification 3-D Secure a échoué.
checkout_already_paidcheckoutnonCe checkout a déjà été payé. Créez-en un nouveau.
checkout_amount_changedcheckoutouiLe montant à payer a changé. Relisez le checkout et affichez le nouveau montant avant de réessayer.
gateway_unavailablepasserelleouiL'acquéreur était injoignable. Erreur transitoire — invitez à réessayer immédiatement.
payment_failedinconnunonLa catégorie non classée. Affichez un message d'échec générique.
invalid_sessionsessionnonLe token du checkout est inconnu ou mal formé.
session_unusablesessionnonLe checkout ne peut plus être utilisé — réglé ou annulé. Créez-en un nouveau.
checkout_unavailablecheckoutouiLe checkout n'a pas pu être atteint.
handshake_timeoutsessionnonLe champ n'a jamais confirmé être prêt, après 20 secondes. Il s'agit généralement d'une page lente — ou d'une origine non enregistrée. Voir Mise en production.
origin_not_allowedsessionnonLe returnUrlError que vous avez envoyé désigne une origine qui n'est pas enregistrée pour votre compte, et aucun wildcard enregistré ne l'admet. Notez que cela concerne uniquement l'origine de retour — une origine d'intégration non enregistrée échoue silencieusement et remonte toujours sous la forme handshake_timeout.

L'absence de webhook signifie « inconnu », pas « échoué ». La livraison est asynchrone et soumise à des nouvelles tentatives, et un paiement peut être réglé sans que vous receviez jamais le webhook — par exemple si votre endpoint était indisponible. Ne déduisez jamais un échec du silence : si le webhook n'est pas arrivé, interrogez le checkout pour obtenir son status avant d'agir.

retriable indique qu'une personne peut réessayer — pas que votre code le peut.

  • Appelez à nouveau submit() à partir d'un nouvel appui sur votre bouton de paiement, sur le même champ monté : l'emplacement de tentative est libéré lorsqu'une tentative aboutit, de sorte que l'appel suivant démarre réellement une nouvelle tentative, et le serveur reste maître de l'identité des tentatives
  • Ne le transformez jamais en boucle de nouvelles tentatives automatiques : un paiement dont vous n'avez pas reçu le résultat a pu aboutir, et le rejouer sans surveillance est précisément ce qui provoque un double débit
  • C'est pourquoi un timeout est retriable: false : un résultat inconnu n'est pas un échec

Node
interface CheckoutError {
  code:      CheckoutErrorCode
  category:  'card' | 'authentication' | 'gateway' | 'checkout' | 'session' | 'unknown'
  message:   string    // unlocalised default; re-translate off `code`, not off this string
  retriable: boolean   // whether a retry can plausibly help
  detail?:   string    // raw provider value, for your logs — never display copy
}

Style et localisation

Votre CSS ne peut pas atteindre l'intérieur de l'iframe — cet isolement est ce qui protège la carte. À la place, le widget accepte un ensemble fixe de tokens, appliqués à ses propres champs. Transmettez-les à mount() ou modifiez-les dynamiquement avec widget.update(). Les tokens que vous omettez conservent les valeurs par défaut du widget ; l'ensemble ci-dessous est exhaustif.

update() accepte theme et locale uniquement. labels est défini à mount() et n'a pas d'équivalent dynamique — seul le handshake le transmet. checkoutToken et layout déterminent quelle frame est démarrée, donc modifier l'un ou l'autre nécessite un nouveau mount(). Chaque update() fusionne son thème avec ce qui est déjà appliqué, de sorte que modifier le style d'un token n'oblige jamais à répéter les autres.

Les libellés sont transmis au moment du montage. Les huit sont modifiables, et tout ce que vous omettez reprend le texte propre au widget pour la locale choisie :

Utilisez des polices système et terminez toujours la pile par une famille générique.

  • Le widget s'affiche dans son propre document : les polices web chargées par votre page ne lui sont donc pas accessibles. Une famille personnalisée telle que 'Inter' est remplacée silencieusement
  • Un nom de famille seul ne laisse aucune famille générique dans la pile, et le navigateur revient alors à sa police par défaut. Écrivez 'Inter, system-ui, sans-serif', jamais 'Inter'
  • mount() lève une erreur si la pile ne se termine pas par une famille générique, plutôt que de laisser l'échec passer inaperçu

Le chargement de votre propre fichier de police dans le champ carte n'est pas pris en charge. Une police fournie depuis l'extérieur peut servir à deviner ce que saisit le porteur de la carte, via des requêtes caractère par caractère. Le widget n'affiche donc que les polices déjà présentes sur l'appareil. Si vous avez besoin d'une police de marque ici, contactez-nous pour qu'elle soit hébergée de notre côté.

L'iframe ajuste elle-même sa taille : elle indique la hauteur de son contenu et ne descend jamais sous une hauteur minimale pendant son démarrage.


Node
widget.update({
  theme: {
    textColor:        '#151B23',  // field text
    placeholderColor: '#8B949E',  // placeholder and unfilled slots
    borderColor:      '#DCE1E7',  // field border
    dangerColor:      '#B4231C',  // error border + text
    backgroundColor:  '#FFFFFF',  // field background
    borderRadius:     '8px',      // field corners, any CSS length
    fontFamily:       'system-ui, sans-serif',
  },
})
Node
window.YavinPaymentsSDK.mount(element, {
  checkoutToken: 'Ab3f...',
  locale: 'fr',
  labels: {
    numberLabel:       'Numéro de carte',
    numberPlaceholder: '1234 5678 9012 3456',
    expiryLabel:       "Date d'expiration",
    expiryPlaceholder: 'MM/AA',
    cvcLabel:          'CVV',
    cvcPlaceholder:    '123',
    numberError:       'Numéro invalide',
    expiryError:       'Date invalide',
  },
})

Sandbox

Chaque compte dispose d'une sandbox qui se comporte comme la production : même widget, mêmes événements et mêmes résultats, y compris l'étape d'authentification renforcée 3-D Secure. Aucune carte n'est débitée et aucun argent n'est transféré. Développez et testez votre intégration dans cet environnement avant de passer aux clés de production.

Ciblez l'environnement sandbox. La sandbox est un environnement distinct avec ses propres URL, mais se comporte par ailleurs exactement comme la production. Créez les checkouts côté serveur sur api.sandbox.yavin.com avec votre clé secrète sandbox (la production utilise api.yavin.com) ; le jeton de checkout obtenu en retour exécute le widget en mode test.

Chargez le widget depuis l'hôte correspondant : https://pay.s.yav.in/sdk/v1/yavin.js en sandbox, https://pay.yav.in/sdk/v1/yavin.js en production. Le runtime détermine l'emplacement de son iframe carte à partir de l'URL de son propre script : un jeton sandbox et un runtime de production ne pourront donc pas communiquer. La mise en production implique de changer les deux URL et votre clé.

Autorisez vos origines sandbox. La liste d'origines autorisées est propre à chaque environnement : enregistrez les domaines qui hébergent le widget dans la liste sandbox, distincte de votre liste de production (voir Mise en production).

Cartes de test

Six numéros de carte forcent un résultat précis sur la passerelle sandbox : rien n'est débité et rien n'atteint une vraie banque. Tout autre numéro, y compris celui d'une vraie carte, est simplement approuvé sans authentification : ces six numéros sont donc les seuls utiles pour vos scénarios de test.

Numéro de carteRésultat
4242424242420042Visa : approuvée immédiatement, sans authentification 3-D Secure
5454545454540042Mastercard : approuvée immédiatement, sans authentification 3-D Secure
6767676767670042CB : approuvée immédiatement, sans authentification 3-D Secure
4242424242420034Visa : authentification renforcée 3-D Secure, avec redirection vers une fausse page d'authentification où vous choisissez Approve ou Decline
5454545454540034Mastercard : authentification renforcée 3-D Secure, avec redirection vers une fausse page d'authentification où vous choisissez Approve ou Decline
6767676767670034CB : authentification renforcée 3-D Secure, avec redirection vers une fausse page d'authentification où vous choisissez Approve ou Decline

Utilisez une date d'expiration future quelconque et un CVC à 3 chiffres quelconque : seul le numéro de carte détermine le résultat.


Mise en production

Enregistrez vos domaines. Le widget ne s'affiche que sur les origines enregistrées pour votre compte, et n'accepte d'instructions que de leur part. Enregistrez toutes les origines qui hébergeront le champ, y compris celles de préproduction. La liste est propre à chaque environnement : la production et la sandbox ont des listes distinctes.

Deux mécanismes indépendants l'appliquent, et tous deux refusent par défaut en cas d'échec : si la recherche échoue pour une raison quelconque, la liste est considérée comme vide plutôt que permissive :

  • l'iframe carte est servie avec un Content-Security-Policy qui désigne vos origines enregistrées comme seuls ancêtres de frame autorisés, de sorte qu'une page non enregistrée ne peut pas afficher la frame ;
  • la frame ignore les instructions envoyées depuis toute origine absente de la liste.

Une entrée est une origine seule : schéma, hôte et port.

  • Le port en fait partie, sauf s'il s'agit du port par défaut du schéma : https://shop.example.com et https://shop.example.com:8443 sont donc deux entrées différentes
  • http en clair n'est accepté que pour l'hôte de boucle locale
  • Une barre oblique finale, un chemin, une chaîne de requête ou des identifiants rendent l'entrée invalide
  • Si vous ne savez pas quoi enregistrer, lisez window.location.origin sur la page qui hébergera le widget et enregistrez exactement cette chaîne

Une seule entrée peut couvrir toute une arborescence de sous-domaines. Un *. en tête correspond aux sous-domaines de toute profondeur : https://*.partner.com autorise https://shop.partner.com et https://eu.shop.partner.com.

Deux choses qu'elle ne fait volontairement pas :

  • Elle n'autorise pas le domaine racine : https://partner.com nécessite sa propre entrée si vous intégrez le widget depuis celui-ci
  • Elle ne correspond qu'à ce suffixe : un domaine trompeur tel que https://partner.com.example.net n'est jamais autorisé

Le schéma et le port doivent toujours correspondre exactement, et le caractère générique doit constituer tout le premier label : https://*.partner.com est valide, https://*partner.com et https://shop.*.com ne le sont pas.

La même entrée couvre returnUrlError : un retour en cas d'échec peut donc atterrir sur n'importe quel sous-domaine qu'autorise un caractère générique enregistré.

Une origine non enregistrée se manifeste par une page lente, pas par une erreur d'autorisation. Les deux mécanismes ci-dessus sont silencieux par conception : la frame n'achève simplement jamais son handshake. Le widget signale donc handshake_timeout après 20 secondes plutôt qu'un code dédié. Si le montage se bloque puis expire dans un environnement que vous venez de déployer, vérifiez d'abord la liste d'origines autorisées.

Confirmez côté serveur. Yavin envoie une requête POST à votre webhook_url à chaque tentative de paiement : payment_received en cas de succès, payment_failed (avec un reason) en cas d'échec. Consultez la référence des webhooks pour connaître la structure de chaque événement.

Le payload payment_received contient le status du checkout :

  • pending : le checkout est encore redevable d'une somme
  • authorised : le paiement est sécurisé mais pas encore capturé
  • ok : les fonds sont capturés et réglés. Un webhook capture signale le passage à ok

Livrez dès que le checkout est authorised ou ok, ou attendez ok si vous exigez des fonds capturés avant de remettre les marchandises. Remettez les marchandises sur la base de cette information côté serveur, jamais sur un résultat dans le navigateur ni sur un paramètre de l'URL de retour.


Référence du widget

Node
// The runtime registers this global as it evaluates.
window.YavinPaymentsSDK.mount(
  container: HTMLElement,          // an element — a selector string is not accepted
  options: {
    checkoutToken: string          // the `checkout_token` from the create-checkout call
    locale?:       'en' | 'fr' | 'es'
    theme?:        Theme
    labels?:       Partial<Labels>
    layout?:       'fields'
    returnUrlError?: string      // 3DS failure return; defaults to this page's origin + pathname
  }
): YavinPaymentsField

interface YavinPaymentsField {
  on(event: 'ready' | 'validity' | 'scheme' | 'result' | 'threeDsChallenge' | 'error', cb): void
  off(event, cb): void
  submit(opts?: { giftAmount?: number }): Promise<PaymentResult>
  update(opts: { theme?: Theme; locale?: 'en' | 'fr' | 'es' }): void
  destroy(): void
}

type PaymentResult =
  | { status: 'succeeded';      transactionId?: string }
  | { status: 'partially_paid'; paidAmount: number; amountLeft: number; transactionId?: string }
  | { status: 'failed';         error: CheckoutError;                   transactionId?: string }

interface Theme {
  textColor?: string; placeholderColor?: string; borderColor?: string
  dangerColor?: string; backgroundColor?: string; borderRadius?: string
  fontFamily?: string   // must end in a CSS generic family
}

interface Labels {
  numberLabel: string; numberPlaceholder: string
  expiryLabel: string; expiryPlaceholder: string
  cvcLabel:    string; cvcPlaceholder:    string
  numberError: string; expiryError:       string
}

// mount() throws synchronously on a missing or blank checkoutToken, an unsupported
// locale, or a fontFamily with no generic family. Everything else is reported
// through the `error` event.
//
// A 3-D Secure step-up resolves via full-page redirect to your return URLs;
// on a step-up submit()'s promise never settles.