Documentation
Tout ce qu'il faut pour encaisser votre premier paiement.
Vue d'ensemble
MOR.AI est le marchand de référence pour vos ventes. Vous créez un paiement, nous collectons la carte et gérons l'authentification, puis nous vous communiquons le résultat.
Les données de carte n'atteignent jamais vos serveurs. Vous nous envoyez un montant et une référence, nous renvoyons une URL, et l'acheteur saisit sa carte sur une page que nous servons. Vos obligations de conformité restent minimales, ce qui est la raison principale de passer par un marchand de référence.
Le déroulé d'un paiement
- Votre serveur appelle
POST /v1/paymentsavec le montant et votre propre référence de commande. - Vous recevez une
checkout_url. Intégrez-la dans une iframe ou redirigez l'acheteur dessus. - L'acheteur saisit sa carte. Si sa banque demande une authentification, nous la gérons.
- Nous envoyons le résultat à votre callback, signé pour que vous puissiez en vérifier l'origine.
Le résultat arrive par callback et non dans la réponse à l'étape un. Un acheteur peut fermer l'onglet après avoir payé alors que le paiement doit encore aboutir : le callback fait foi.
Où envoyer vos requêtes
| API | https://api.trymor.ai |
|---|---|
| Checkout | https://pay.trymor.ai, renvoyé sous forme d'URL complète |
| Tableau de bord | https://app.trymor.ai |
L'hôte est le même dans les deux environnements : c'est votre clé qui détermine dans lequel vous êtes.
Environnements
| Sandbox | Clés mor_test_…. Aucun mouvement d'argent. Utilisez les cartes de test plus bas. |
|---|---|
| Production | Clés mor_live_…. Vraies cartes, vrai argent. |
Une clé sandbox ne fonctionne pas en production et inversement : une clé collée dans la mauvaise configuration échoue immédiatement plutôt qu'après un débit réel.
Authentification
Chaque requête porte votre clé d'API en bearer token.
Authorization: Bearer mor_test_VOTRE_CLE_ICI
La clé vous identifie et constitue le seul identifiant nécessaire. Gardez-la sur votre serveur : elle peut créer des paiements et lire tous ceux que vous avez effectués, elle n'a donc sa place ni dans un navigateur, ni dans une application mobile, ni nulle part qu'un client puisse lire.
Nous ne stockons qu'une empreinte de votre clé et ne pouvons donc pas la retrouver. Si vous la perdez ou la pensez compromise, demandez-nous : nous en émettons une nouvelle et révoquons l'ancienne.
Créer un paiement
Un seul appel, depuis votre serveur.
POST https://api.trymor.ai/v1/payments
Authorization: Bearer mor_test_VOTRE_CLE_ICI
Idempotency-Key: order-1043-attempt-1
Content-Type: application/json
{
"amount": 4999,
"currency": "EUR",
"reference": "order-1043",
"return_url": "https://votreboutique.com/commande/1043/terminee",
"customer": {
"email": "marie@example.com",
"address_line1": "12 Rue de Rivoli",
"city": "Paris",
"postal_code": "75001",
"country": "FR"
}
}
Champs
| amount | Entier, dans la plus petite unité de la devise. 4999 vaut 49,99 EUR. Ni décimal ni chaîne de caractères : un entier ne peut pas être arrondi en silence par quoi que ce soit entre vous et nous. |
|---|---|
| currency | Code ISO à trois lettres, par exemple EUR. |
| reference | Votre identifiant de commande. Repris dans chaque réponse et chaque callback, vous n'avez donc jamais à stocker nos identifiants pour vous y retrouver. |
| return_url | Où l'acheteur revient une fois terminé. Doit être en https. |
| description | Facultatif. N'est montré à personne ; conservé pour vos archives. |
| metadata | Facultatif. Jusqu'à 20 clés de vos propres données, restituées telles quelles. |
| customer | Facultatif, et vaut la peine d'être envoyé. Voir ci-dessous. |
Le bloc customer
"customer": {
"email": "marie@example.com",
"phone": "+33612345678",
"address_line1": "12 Rue de Rivoli",
"address_line2": "Apt 4",
"city": "Paris",
"state": "Île-de-France",
"postal_code": "75001",
"country": "FR"
}
Chaque champ est facultatif, et le bloc lui-même l'est aussi. Les paiements fonctionnent sans.
Envoyez-le quand même. C'est la banque émettrice qui décide d'accepter un paiement et d'exiger ou non une authentification, et elle décide avec ce qu'on lui donne. Avec une adresse qui correspond à la carte, davantage de paiements passent directement. Sans, davantage basculent vers un challenge 3DS — et c'est là que les acheteurs abandonnent.
Cela compte aussi après la vente. Un chargeback se conteste avec des éléments sur qui a acheté quoi, et « nous ne savons pas qui c'était » n'est pas une défense.
country est un code ISO à deux lettres. Le reste est du texte libre, tel
qu'il figure sur le relevé bancaire ou l'étiquette d'expédition.
Réponse
201 Created
{
"id": "b0ddaea2-b485-443b-86ad-168c0635fcdb",
"status": "created",
"amount": 4999,
"currency": "EUR",
"reference": "order-1043",
"checkout_url": "https://pay.trymor.ai/checkout/a17fa826-30c3-4f5c-85b1-974d03da17bf",
"expires_at": "2026-08-12T11:36:02Z",
"created_at": "2026-08-12T11:06:02Z"
}
L'URL de checkout est valable trente minutes. Passé ce délai, créez un nouveau paiement.
Idempotence
L'en-tête Idempotency-Key est obligatoire. Choisissez une valeur liée à la
commande et à la tentative, par exemple order-1043-attempt-1.
Si votre connexion tombe, vous n'avez aucun moyen de savoir si nous avons créé le paiement.
Réessayez avec la même clé et vous récupérez la réponse d'origine, avec le même id
et la même checkout_url, plutôt qu'un second débit. La réponse porte
Idempotent-Replay: true pour que vous puissiez le distinguer.
Réutiliser une clé avec un corps différent est une erreur, pas un rejeu : nous débiterions sinon pour une commande en répondant à propos d'une autre.
Créer un paiement
Un seul appel, depuis votre serveur.
POST https://api.trymor.ai/v1/payments
Authorization: Bearer mor_test_VOTRE_CLE_ICI
Idempotency-Key: order-1043-attempt-1
Content-Type: application/json
{
"amount": 4999,
"currency": "EUR",
"reference": "order-1043",
"return_url": "https://votreboutique.com/order/1043/complete",
"customer": {
"email": "marie@example.com",
"address_line1": "12 Rue de Rivoli",
"city": "Paris",
"postal_code": "75001",
"country": "FR"
}
}
Champs
| amount | Entier, dans la plus petite unité de la devise. 4999 vaut 49,99 EUR. Ni décimal ni chaîne : un entier ne peut pas être arrondi en silence par quoi que ce soit entre vous et nous. |
|---|---|
| currency | Code ISO à trois lettres, par exemple EUR. |
| reference | Votre propre identifiant de commande. Renvoyé dans chaque réponse et chaque callback, vous n'avez donc jamais à stocker nos identifiants pour rapprocher. |
| return_url | Où l'acheteur arrive une fois terminé. Doit être en https. |
| description | Facultatif. Montré à personne ; conservé pour vos archives. |
| metadata | Facultatif. Jusqu'à 20 clés de vos propres données, renvoyées telles quelles. |
| customer | Facultatif, et à envoyer. Voir ci-dessous. |
Le bloc customer
"customer": {
"email": "marie@example.com",
"phone": "+33612345678",
"address_line1": "12 Rue de Rivoli",
"address_line2": "Apt 4",
"city": "Paris",
"state": "Île-de-France",
"postal_code": "75001",
"country": "FR"
}
Chaque champ est facultatif, le bloc lui-même aussi. Les paiements fonctionnent sans.
Envoyez-le quand même. C'est la banque émettrice qui décide d'approuver un paiement et d'imposer ou non une authentification, et elle décide sur ce qu'on lui donne. Avec une adresse qui correspond à la carte, davantage de paiements passent directement. Sans, davantage basculent en challenge 3DS — et c'est là que les acheteurs abandonnent.
Cela compte aussi après la vente. Un chargeback se conteste avec des preuves sur qui a acheté quoi, et « nous ne savons pas qui c'était » n'est pas une défense.
country est un code ISO à deux lettres. Le reste est du texte libre, tel
qu'imprimé sur le relevé bancaire ou l'étiquette de livraison.
Réponse
201 Created
{
"id": "b0ddaea2-b485-443b-86ad-168c0635fcdb",
"status": "created",
"amount": 4999,
"currency": "EUR",
"reference": "order-1043",
"checkout_url": "https://pay.trymor.ai/checkout/a17fa826-30c3-4f5c-85b1-974d03da17bf",
"expires_at": "2026-08-12T11:36:02Z",
"created_at": "2026-08-12T11:06:02Z"
}
L'URL de checkout est valable trente minutes. Passé ce délai, créez un autre paiement.
Idempotence
L'en-tête Idempotency-Key est obligatoire. Choisissez quelque chose lié à la
commande et à la tentative, comme order-1043-attempt-1.
Si votre connexion tombe, vous n'avez aucun moyen de savoir si le paiement a été créé.
Réessayez avec la même clé et vous récupérez la réponse d'origine, avec le même
id et la même checkout_url, plutôt qu'un second débit. La réponse
porte Idempotent-Replay: true pour que vous puissiez le distinguer.
Réutiliser une clé avec un corps différent est une erreur, pas un rejeu : nous débiterions sinon pour une commande en répondant à propos d'une autre.
Afficher le formulaire de paiement
Intégrez la checkout_url dans une iframe, ou redirigez l'acheteur dessus. L'iframe est généralement préférable : l'acheteur reste sur votre site.
<iframe
src="https://pay.trymor.ai/checkout/a17fa826-…"
style="width:100%;height:560px;border:0"
allow="payment">
</iframe>
La page ne porte aucun montant dans son URL : rien de ce qu'envoie un navigateur ne peut changer ce qui est débité. Elle se charge en un seul document sans requête externe, ce qui compte pour les acheteurs en connexion lente.
L'authentification sort du cadre
Quand une banque demande une authentification, beaucoup d'émetteurs refusent de s'afficher
dans une iframe. Nous naviguons alors la fenêtre principale, et l'acheteur revient ensuite sur
votre return_url. Votre page doit supporter d'être quittée puis retrouvée.
Dimensionner le cadre
Le cadre vous indique la hauteur dont il a besoin, au chargement et à chaque changement — un message de validation qui apparaît le rend plus haut. Redimensionnez plutôt que de deviner une hauteur, sinon l'acheteur se retrouve à faire défiler dans une boîte.
window.addEventListener("message", function (event) {
if (event.origin !== "https://pay.trymor.ai") return;
if (event.data.type === "mor:height") {
frame.style.height = event.data.height + "px";
}
});
Utiliser votre propre bouton
Par défaut le cadre affiche le montant et un bouton de paiement. Si votre checkout a déjà
son bouton et que vous voulez seulement nos champs de carte, passez le mode à
fields sous Checkout dans votre tableau de bord.
Dans ce mode, votre bouton soumet le cadre :
payButton.addEventListener("click", function () {
frame.contentWindow.postMessage(
{ type: "mor:submit" },
"https://pay.trymor.ai"
);
});
Une chose change alors. En mode fields, le montant que voit l'acheteur est celui que vous affichez, pas le nôtre — assurez-vous qu'il corresponde à celui envoyé à la création. Nous débitons ce que dit le paiement, quoi qu'il arrive.
Langue
Le checkout s'affiche dans la langue du navigateur de l'acheteur : anglais, français, allemand, italien, espagnol et néerlandais. Une langue non prise en charge bascule en anglais plutôt que d'afficher des libellés vides.
Si vos acheteurs doivent toujours voir une seule langue quel que soit leur navigateur, fixez-la sous Checkout dans votre tableau de bord. C'est généralement le bon choix quand vous vendez sur un seul marché.
Apparence
Réglable sous Checkout dans votre tableau de bord, à côté d'un aperçu qui se redessine à chaque changement. Polices, couleurs, rayon des angles et hauteur des champs, pour que le formulaire s'accorde au reste de votre checkout. Nous acceptons des valeurs nommées et non du CSS : une feuille de style sur une page qui reçoit des numéros de carte pourrait masquer un champ ou superposer un autre input au nôtre.
Savoir ce qui s'est passé, côté navigateur
Le cadre envoie un message à votre page quand il a terminé. Servez-vous-en pour mettre à jour votre interface, pas pour décider si vous avez été payé : un navigateur peut être fermé, et seul le callback fait foi.
window.addEventListener("message", function (event) {
if (event.origin !== "https://pay.trymor.ai") return;
if (event.data.type === "mor:result") {
// Affichez un indicateur et attendez la confirmation de votre serveur.
}
});
3DS et 2D
Un paiement emprunte l'une de deux voies.
3DS
Le mode par défaut, et celui à utiliser pour tout votre trafic. La banque vérifie le porteur avant l'autorisation : un acheteur qui prétend ensuite ne pas avoir fait l'achat devient le problème de la banque, pas le vôtre.
2D
Aucune étape de vérification. Le taux d'acceptation est meilleur et l'acheteur n'a pas d'écran supplémentaire, mais il n'y a aucune protection : chaque « je n'ai pas autorisé ça » revient vers vous.
Tolérance zéro sur le 2D. Il s'active compte par compte, uniquement pour des clients connus et récurrents. Envoyez-y du trafic froid ou non testé et le compte est fermé.
Plafonné à 10. Au-delà, le paiement passe en 3DS quoi que vous envoyiez : cette voie sert aux commandes de test, pas aux ventes.
Demandez-nous de l'activer sur votre compte. Une fois actif, envoyez
"rail": "2d" à la création du paiement pour les commandes concernées. Tout ce qui
ne le porte pas passe en 3DS, comme une demande 2D sur un compte non activé.
{
"amount": 4999,
"currency": "EUR",
"reference": "order-1043",
"rail": "2d",
"return_url": "https://votreboutique.com/merci"
}
Traiter le callback
Quand un paiement atteint un état final, nous appelons votre callback. C'est le résultat qui fait foi.
POST https://votreboutique.com/webhooks/mor
Mor-Signature: 4f8a3c…
Mor-Timestamp: 1786532801
Mor-Delivery-Id: 587299e9-dc68-463b-8e9a-40f90eb593f4
Mor-Event-Type: payment.succeeded
Content-Type: application/json
{
"id": "587299e9-dc68-463b-8e9a-40f90eb593f4",
"type": "payment.succeeded",
"payment_id": "b0ddaea2-b485-443b-86ad-168c0635fcdb",
"reference": "order-1043",
"status": "succeeded",
"amount": 4999,
"currency": "EUR",
"created_at": "2026-08-12T11:06:41Z"
}
Votre clé de signature
Elle apparaît sous Checkout dans votre tableau de bord dès que vous enregistrez une URL de callback. Émise une fois et jamais modifiée : ce que vous construisez dessus continue de fonctionner.
Vérifier la signature
Faites-le avant d'agir sur quoi que ce soit dans le corps. Sans cela, quiconque découvre votre endpoint peut vous annoncer un paiement réussi.
La signature est un HMAC-SHA256 sur l'horodatage, un point, et le corps brut de la requête, avec votre clé webhook. Utilisez les octets tels que reçus : parser puis re-sérialiser le JSON les modifie et la signature ne correspondra plus.
// Node
const crypto = require("crypto");
function verify(rawBody, signature, timestamp, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
# Python
import hmac, hashlib
def verify(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
timestamp.encode() + b"." + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Comparez en temps constant, comme dans les deux exemples. Une comparaison qui sort tôt indique à un attaquant quelle part d'une signature forgée était juste.
Rejetez tout horodatage vieux de plus de quelques minutes. L'horodatage fait partie du contenu signé et ne peut donc pas être modifié, ce qui en fait une protection efficace contre le rejeu d'une livraison interceptée.
Soyez idempotent
Nous réessayons jusqu'à recevoir un 2xx : vous verrez donc parfois deux fois la même
livraison. Mor-Delivery-Id est stable d'une tentative à l'autre — enregistrez-le
et ignorez les répétitions.
Répondez vite
Renvoyez un 2xx dès que l'événement est enregistré, puis faites le reste de votre travail. Toute autre réponse, ou aucune réponse en quinze secondes, compte comme un échec et nous réessayons.
Réessais
Huit tentatives sur environ une journée : après 30 secondes, puis 2, 10 et 30 minutes, puis
2, 6 et 12 heures. Si votre endpoint est indisponible une heure, vous ne perdez rien. Passé
cela nous arrêtons, et il vous faut rapprocher avec
GET https://api.trymor.ai/v1/payments/{id}.
Remboursements
Remboursez tout ou partie d'un paiement. Un paiement peut être remboursé plusieurs fois, jusqu'à concurrence de ce qu'il en reste.
POST https://api.trymor.ai/v1/payments/{payment_id}/refunds
Authorization: Bearer mor_test_VOTRE_CLE_ICI
Content-Type: application/json
{
"reference": "refund-1043-1",
"amount": 2000,
"reason": "Retour d'un article"
}
| reference | Obligatoire. Votre identifiant pour ce remboursement. Deux remboursements sur un même paiement doivent se distinguer dans vos registres ; réutiliser une référence est refusé plutôt que de créer un second remboursement. |
|---|---|
| amount | Facultatif. Entier en unités mineures. Omettez-le pour rembourser tout ce qui reste remboursable, ce qui est le cas le plus courant. |
| reason | Facultatif. Conservé pour vos archives et transmis. |
Réponse
202 Accepted
{
"id": "9c1f2e3a-...",
"payment_id": "b0ddaea2-...",
"status": "requested",
"amount": 2000,
"currency": "EUR",
"reference": "refund-1043-1",
"remaining": 2999,
"created_at": "2026-08-18T15:04:02Z"
}
202 et non 201, délibérément : le remboursement est enregistré et en route, pas terminé.
Traiter cette réponse comme une confirmation vous ferait annoncer au client que son argent est
revenu avant qu'il ne soit parti. Le résultat arrive par callback, en
refund.succeeded ou refund.failed.
remaining indique ce qui reste remboursable après celui-ci : pas besoin d'un
second appel pour le savoir.
Vérifier ce qui est remboursable
GET https://api.trymor.ai/v1/payments/{payment_id}/refundable
Authorization: Bearer mor_test_VOTRE_CLE_ICI
{
"payment_id": "b0ddaea2-...",
"paid": 4999,
"refunded": 0,
"pending": 2000,
"remaining": 2999,
"currency": "EUR"
}
pending correspond aux remboursements demandés mais pas encore aboutis. Ils
sont retenus sur le solde, ce qui explique que remaining puisse être inférieur à
paid moins refunded. Sans cela, deux remboursements demandés au même
instant pourraient ensemble dépasser le paiement.
À quoi s'attendre
| Frais | Notre commission n'est pas restituée sur un remboursement. Un remboursement vous coûte la vente, pas la vente plus notre marge. |
|---|---|
| Partiel | Remboursez autant de fois que nécessaire jusqu'à épuisement. Le paiement passe en partially_refunded, puis refunded. |
| Échecs | Un remboursement refusé libère sa retenue : le montant redevient remboursable et vous pouvez corriger puis réessayer. |
| Paiements non aboutis | Seul un paiement encaissé peut être remboursé. Un paiement encore en autorisation ou en attente de confirmation est refusé : rembourser un débit qui n'existe peut-être pas revient à envoyer de l'argent pour rien. |
Statuts
| created | Le paiement existe. Personne n'a saisi de carte. |
|---|---|
| processing | Une carte a été saisie et l'autorisation est en cours. |
| requires_3ds | L'acheteur s'authentifie auprès de sa banque. |
| succeeded | Payé. État final. |
| failed | Refusé ou abandonné. État final. |
| refunded | Intégralement remboursé. |
| partially_refunded | Remboursé en partie. |
| disputed | Le porteur a ouvert un chargeback. |
| indeterminate | Nous ne savons pas encore. À traiter comme ni payé ni échoué. |
indeterminate mérite d'être compris plutôt qu'ignoré. Cela signifie qu'un
appel n'a pas abouti et que la carte a peut-être été débitée, ou non. L'annoncer comme un échec
serait pire : vous diriez au client qu'il ne s'est rien passé alors que sa banque dit le
contraire. Nous le résolvons par rapprochement, généralement en quelques minutes, puis nous
envoyons le callback.
Les callbacks arrivent pour payment.succeeded, payment.failed,
payment.refunded, refund.succeeded et
refund.failed. Les états intermédiaires sont visibles sur
GET https://api.trymor.ai/v1/payments/{id} si vous en avez besoin.
Lire un paiement
GET https://api.trymor.ai/v1/payments/b0ddaea2-b485-443b-86ad-168c0635fcdb
Authorization: Bearer mor_test_VOTRE_CLE_ICI
200 OK
{
"id": "b0ddaea2-b485-443b-86ad-168c0635fcdb",
"status": "succeeded",
"amount": 4999,
"currency": "EUR",
"reference": "order-1043",
"created_at": "2026-08-12T11:06:02Z",
"updated_at": "2026-08-12T11:06:41Z"
}
L'historique complet
Chaque étape d'un paiement est enregistrée et consultable. Utile quand un client demande ce qui s'est passé, et c'est là-dessus qu'un litige se plaide.
GET https://api.trymor.ai/v1/payments/{id}/events
Authorization: Bearer mor_test_VOTRE_CLE_ICI
{
"payment_id": "b0ddaea2-…",
"events": [
{ "seq": 1, "kind": "created", "summary": "Paiement créé pour 49,99 EUR, référence marchand order-1043" },
{ "seq": 2, "kind": "payment_submitted", "summary": "Carte saisie et transmise, visa se terminant par 0010" },
{ "seq": 3, "kind": "redirect_issued", "summary": "Acheteur envoyé vers l'émetteur pour authentification, parcours sans friction" },
{ "seq": 4, "kind": "notification_received", "summary": "Résultat SUCCESS reçu pour 49,99 EUR" },
{ "seq": 5, "kind": "state_changed", "summary": "État passé de processing à succeeded" }
]
}
Erreurs
Toutes les erreurs ont la même forme.
{
"error": {
"code": "invalid_request",
"message": "Amount must be a positive integer in the currency's minor unit for example 899 for 8.99 EUR.",
"field": "amount",
"trace_id": "5d5d63e3fea7b258"
}
}
Branchez sur code. Lisez message dans vos logs. Citez
trace_id quand vous nous écrivez : cela nous suffit pour retrouver la requête,
sans horodatage ni approximation.
| 400 invalid_request | Quelque chose ne va pas dans la requête. field précise quoi. |
|---|---|
| 401 unauthorized | Clé absente, inconnue ou révoquée, ou mauvais environnement. |
| 403 forbidden | Compte validé mais pas encore ouvert aux paiements. |
| 404 not_found | Paiement inexistant, ou qui ne vous appartient pas. |
| 409 idempotency_key_reused | Cette clé a été utilisée avec un corps différent. |
| 502 bank_unavailable | Pas de réponse en amont. Vous pouvez réessayer avec la même clé d'idempotence. |
| 500 internal_error | De notre côté. Citez le trace_id. |
Cartes de test
Sandbox uniquement. N'importe quelle date d'expiration future et n'importe quel CVC, sauf mention contraire.
| 4000 0000 0000 0002 | Acceptée. |
|---|---|
| 4000 0000 0000 0010 | Acceptée après authentification sans friction. |
| 4000 0000 0000 0028 | Challenge d'authentification. |
| 4000 0000 0000 0036 | Refusée pendant l'authentification. |
| 4000 0000 0000 0044 | Refusée, provision insuffisante. |
| 4000 0000 0000 0051 | Aucune réponse. Le paiement reste non résolu, comme si nous n'avions jamais eu de retour. À tester : c'est le cas qui coûte de l'argent en production et celui que personne n'essaie. |
N'importe quel montant fonctionne en sandbox.
Les callbacks en développement
Votre URL de callback doit être joignable depuis internet et en https : localhost ne
fonctionnera pas. Utilisez un tunnel comme cloudflared ou ngrok
pendant le développement.
Avant la mise en production
- Votre clé de production est sur votre serveur uniquement, et pas dans le dépôt.
- Vous vérifiez la signature du callback et rejetez tout ce qui échoue.
- Vous ignorez un
Mor-Delivery-Iddéjà vu plutôt que de le traiter deux fois. - Vous répondez 2xx aux callbacks avant de faire votre propre travail.
- Vous considérez le callback comme le résultat, pas le navigateur.
- Vos clés d'idempotence sont uniques par tentative et vous réessayez avec la même.
- Vous nous avez indiqué le site sur lequel vos clients achètent, exactement tel qu'ils le voient.
Le dernier point compte plus qu'il n'y paraît : c'est ce qui apparaît sur le relevé bancaire de votre client, et un relevé qu'un client ne reconnaît pas est la première cause de chargeback.