Tapaya
Paiements en ligneGuide d’intégrationEmbedded Checkout

Embedded Checkout

Ajoutez des champs de carte sécurisés ainsi qu’Apple Pay ou Google Pay à votre propre page de paiement.

Placez des champs de carte sécurisés dans votre propre page de paiement. Le formulaire et le bouton d’envoi vous appartiennent ; Tapaya gère la validation de la carte, la tokenisation et 3-D Secure. Les paiements intégrés sont en production pour les commerçants approuvés, et la sandbox est disponible pour les tests. Ce guide utilise Tapaya.js 0.2.0 et le SDK Node.js 0.3.0.

Your frontend
Your backend
Tapaya
  1. 1. Your frontend to Your backend: Prepare payment.
  2. 2. Your backend to Tapaya: Create or resume payment.
  3. 3. Tapaya to Your backend: Payment and client secret.
  4. 4. Your backend to Your frontend: Client secret and status.
  5. 5. Your frontend to Tapaya: Confirm card and 3-D Secure.
  6. 6. Tapaya to Your frontend: Payment outcome.
  7. 7. Your backend to Tapaya: Read order's payment.
  8. 8. Tapaya to Your backend: Payment status.
  9. 9. Your backend to Your frontend: Show verified result.

Moyens de paiement

L’Embedded Checkout prend en charge les cartes, Apple Pay et Google Pay. Utilisez votre propre bouton de paiement pour les cartes et les boutons officiels des portefeuilles pour Apple Pay et Google Pay. Voir Ajouter Apple Pay et Google Pay.

Avant de commencer

Installer le SDK

Un seul paquet contient le client serveur, le SDK navigateur et le hook React :

npm install @tapayadot/checkout
ImportOù
@tapayadot/checkoutVotre serveur uniquement. L’importer dans un build navigateur lève une erreur, de sorte que votre clé API n’atteint jamais une page.
@tapayadot/checkout/clientVotre page de paiement.
@tapayadot/checkout/reactVotre page de paiement, si vous utilisez React.

Configurer votre serveur

Les paiements intégrés n’ont pas besoin d’URL de retour. Les exemples serveur ci-dessous existent en versions HTTP (fetch), Node.js SDK et cURL, qui appellent toutes la même API commerçant et ne modifient pas les champs de carte du navigateur.

Node.js SDKHTTP (fetch) et cURL
CléTAPAYA_SECRET_KEY, lue par new Tapaya()TAPAYA_CHECKOUT_API_KEY
EnvironnementTAPAYA_ENVIRONMENT=sandbox ou production. Sans cette variable, une clé tapaya_live_ sélectionne la production et les autres clés utilisent la sandbox par défaut.TAPAYA_CHECKOUT_API_URL : https://api.sandbox.tapaya.com ou https://api.tapaya.com
Mode plateformeTransmettez le merchantToken de la commande à chaque appel, ou définissez-le avec new Tapaya(key, { merchantToken })Envoyez X-Tapaya-Merchant-Token dans chaque requête. La fonction utilitaire fetch le lit depuis TAPAYA_MERCHANT_TOKEN.

Une clé checkout de commerçant ne nécessite rien d’autre. En mode plateforme, utilisez la Secret key de l’organisation, et faites en sorte qu’un backend qui sert plusieurs commerçants choisisse le jeton à partir de la commande authentifiée. Voir Authentification du Checkout. Les client secrets sont propres à chaque paiement.

Effectuez l’onboarding avec les paiements en ligne et vérifiez que le commerçant est Ready dans les paramètres du Checkout.

Autoriser Tapaya dans votre CSP

Le chargeur récupère Tapaya.js 0.2.0 depuis https://js.tapaya.com/checkout/0.2.0/checkout.js ; la version du script navigateur est indépendante de la version du SDK Node.js. Ajoutez l’origine des ressources de Tapaya à votre Content Security Policy existante :

script-src https://js.tapaya.com
frame-src https://js.tapaya.com

Le navigateur reçoit la publishable key et le client secret propre au paiement. Votre clé API secrète de commerçant reste sur votre serveur, et les données de carte ainsi que les scripts du prestataire de paiement restent dans la frame de Tapaya. Pour les tests en local, servez Tapaya.js vous-même et transmettez son origine au chargeur, par exemple loadTapaya(publishableKey, { apiOrigin, origin: 'http://localhost:4191' }).

Fonctionnement

  1. Votre page monte les champs de carte de Tapaya et les fait pointer vers un endpoint de votre serveur.
  2. Au chargement de la page et lorsque le client paie, les champs de carte appellent cet endpoint. Celui-ci renvoie le paiement de la commande sous la forme d’un clientSecret : un identifiant valable pour un seul paiement, que les champs de carte utilisent pour confirmer le paiement directement auprès de Tapaya. Le code de votre page ne le lit jamais.
  3. Votre page suit l’outcome du paiement. Lorsqu’il est terminé, votre serveur vérifie le paiement avant que vous ne traitiez la commande.

Accepter un paiement

Obtenir les paramètres du navigateur

Récupérez une fois les paramètres du navigateur sur votre serveur et ajoutez-les à la configuration de votre frontend.

Conservez cette fonction utilitaire sur votre serveur. Elle vérifie les erreurs HTTP, traite un paiement de commande absent comme null lorsque c’est demandé, et ne relance pas automatiquement les requêtes.

server/tapaya.js
const apiOrigin = new URL(process.env.TAPAYA_CHECKOUT_API_URL).origin;
const apiKey = process.env.TAPAYA_CHECKOUT_API_KEY;
const merchantToken = process.env.TAPAYA_MERCHANT_TOKEN;

export async function paymentRequest(path, { method = 'GET', body, key, allowMissing = false } = {}) {
	const response = await fetch(new URL(path, apiOrigin), {
		method,
		headers: {
			Authorization: `Bearer ${apiKey}`,
			...(merchantToken ? { 'X-Tapaya-Merchant-Token': merchantToken } : {}),
			...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
			...(key ? { 'Idempotency-Key': key } : {}),
		},
		...(body === undefined ? {} : { body: JSON.stringify(body) }),
		redirect: 'error',
		cache: 'no-store',
		signal: AbortSignal.timeout(35000),
	});
	if (allowMissing && response.status === 404) return null;
	if (!response.ok) throw new Error(`Payment request failed (${response.status})`);
	return response.json();
}

const { publishableKey } = await paymentRequest('/merchant/payments/configuration');
export const config = { publishableKey, apiOrigin };

L’API renvoie publishableKey et environment. Définissez apiOrigin à partir de l’URL d’API que vous avez appelée ; ce n’est pas un champ de la réponse de configuration.

Les deux valeurs peuvent être incluses sans risque dans votre page. Les publishable keys utilisent pk_test_ avec les clés de passerelle de test et pk_live_ avec les clés de passerelle de production. Transmettez toujours au chargeur l’apiOrigin renvoyée par votre serveur, afin que le navigateur et le serveur restent sur le même environnement.

Ajouter les champs de carte

Ajoutez un conteneur à votre formulaire de paiement existant :

checkout.html
<form id="checkout">
	<!-- Your other checkout inputs go here. -->
	<div id="card-fields"></div>
	<button type="submit" disabled>Pay</button>
	<p id="message" role="status"></p>
</form>

Chargez Tapaya et montez les champs. prepare est l’endpoint que vous ajoutez à l’étape suivante.

checkout.ts
import { loadTapaya } from '@tapayadot/checkout/client'
import { config } from './config' // { publishableKey, apiOrigin } from your server

const form = document.querySelector<HTMLFormElement>('#checkout')!
const button = form.querySelector<HTMLButtonElement>('button')!
const message = document.querySelector<HTMLElement>('#message')!
const tapaya = await loadTapaya(config.publishableKey, { apiOrigin: config.apiOrigin })
const payment = tapaya.createPayment({
	prepare: '/api/payments',
	onChange: ({ canSubmit, outcome }) => {
		button.disabled = !canSubmit
		if (outcome === 'completed') location.assign('/checkout/confirmation')
		if (outcome === 'ended') message.textContent = 'This payment cannot be completed. Start a new checkout.'
	},
})
await payment.mount('#card-fields')

Lorsque les champs sont montés, ils appellent prepare pour reprendre le paiement existant de la commande. Un rechargement pendant un paiement ne débite jamais une seconde fois : la page affiche plutôt l’état actuel du paiement.

Renvoyer le paiement de la commande

Reliez ce gestionnaire à POST /api/payments. getCheckoutOrder() est une fonction de votre propre application : authentifiez le client, vérifiez son accès à sa commande et renvoyez les valeurs enregistrées id, amount, currency et status. Appliquez votre protection CSRF habituelle à la route.

Lisez le paiement existant avant de créer une tentative. Réutilisez un paiement non résolu, et ne créez une autre tentative qu’après un refus ou une annulation avec retryAllowed: true.

server/payments.js
import { paymentRequest } from './tapaya.js';
import { getCheckoutOrder } from './orders.js';

export async function findPayment(order) {
	const query = new URLSearchParams({ merchantOrderId: order.id });
	const payment = await paymentRequest(`/merchant/payments?${query}`, { allowMissing: true });
	if (payment && (payment.merchantOrderId !== order.id ||
		payment.amount !== order.amount || payment.currency !== order.currency)) {
		throw new Error('Payment does not match the order');
	}
	return payment;
}

export function browserStatus(payment) {
	if (payment.status === 'successful') return 'completed';
	if (['declined', 'cancelled'].includes(payment.status) && !payment.retryAllowed) return 'ended';
	return 'open';
}

export async function POST(request) {
	const order = await getCheckoutOrder(request);
	const payable = order.status === 'pending_payment';
	let payment = await findPayment(order);
	if (payable && (!payment ||
		(['declined', 'cancelled'].includes(payment.status) && payment.retryAllowed))) {
		// Derive the same key on retries, including after a lost creation response.
		let key = `${order.id}:${payment ? `after:${payment.id}` : 'initial'}`;
		if (/[^\x21-\x7e]/.test(key)) {
			const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(key));
			key = `prepare:${Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')}`;
		}
		payment = await paymentRequest('/merchant/payments', {
			method: 'POST', key,
			body: { merchantOrderId: order.id, amount: order.amount, currency: order.currency },
		});
		if (payment.merchantOrderId !== order.id || payment.amount !== order.amount || payment.currency !== order.currency) {
			throw new Error('Payment does not match the order');
		}
	}
	if (!payment) return Response.json({ error: 'Order cannot accept payment' }, { status: 409 });
	return Response.json(
		{ clientSecret: payable ? payment.clientSecret : null, status: browserStatus(payment) },
		{ headers: { 'Cache-Control': 'no-store' } },
	);
}

Utilisez un code de devise en majuscules et un ID de commande enregistré de 200 caractères au maximum. Ne modifiez pas les informations de la commande une fois le paiement commencé. En cas de dépassement de délai, relisez le paiement de la commande avant de décider s’il faut une nouvelle tentative. Ne générez jamais de nouvelle clé simplement parce qu’une réponse a été perdue.

L’API REST renvoie des états de paiement. Le SDK Node les convertit en status attendu par les champs de carte ; les intégrations fetch et cURL appliquent la même correspondance dans leur propre endpoint prepare :

status de l’APIstatus de l’endpoint prepare
successfulcompleted
declined ou cancelled, avec retryAllowed: falseended
ready, pending, action_required, ou un refus ou une annulation permettant une nouvelle tentativeopen

Soumettre depuis votre gestionnaire

Ajoutez ceci au même script. Votre gestionnaire valide votre formulaire et appelle submit() :

checkout.ts
form.addEventListener('submit', async (event) => {
	event.preventDefault()
	if (!form.reportValidity() || !payment.snapshot.canSubmit) return
	await payment.submit().catch(() => {
		// The card fields show the reason. The shopper can try again when canSubmit allows it.
	})
})

submit() appelle prepare pour le paiement, puis valide la carte, exécute 3-D Secure et confirme le paiement auprès de Tapaya.

Afficher le résultat

onChange reçoit le snapshot du paiement à chaque changement, y compris lorsque la page se charge avec un paiement déjà finalisé. Suivez snapshot.outcome :

outcomeCe qu’il faut afficher
openLe bouton de paiement, activé tant que canSubmit est vrai. Un refus reste open, et le submit() suivant obtient une nouvelle tentative.
pendingUn indicateur de progression. Le résultat n’est pas encore connu, rien ne peut donc être payé à nouveau. Le SDK vérifie automatiquement le statut tant que la page est visible.
completedVotre page de confirmation. Votre serveur y vérifie la commande.
endedUn nouveau parcours de paiement. Aucune autre tentative n’est possible pour cette commande.

Appelez payment.destroy() lorsque votre vue de paiement est démontée. Pour React, utilisez usePayment() de @tapayadot/checkout/react. Il accepte les mêmes options plus publishableKey et apiOrigin, et renvoie un fieldRef pour le conteneur, le snapshot actuel et submit().

Vérifier avant de traiter la commande

Le résultat côté navigateur sert à l’affichage. Sur votre page de confirmation, lisez le paiement de la commande sur votre serveur, et ne traitez la commande que lorsque le paiement est terminé :

server/confirmation.js
import { findPayment } from './payments.js';

const order = await getCheckoutOrder(request);
const payment = await findPayment(order); // Checks order ID, amount, and currency.
if (payment?.status === 'successful') await markOrderPaid(order.id, payment.id);

Faites en sorte que markOrderPaid() n’enregistre le paiement qu’une seule fois, même s’il est appelé à plusieurs reprises, et déclenchez le traitement de la commande à partir de cet état enregistré.

Rapprochez les commandes dont les clients partent

Un client peut fermer l’onglet avant l’ouverture de la page de confirmation. Exécutez une tâche planifiée qui appelle verify() ou lit GET /merchant/payments?merchantOrderId=... pour les commandes impayées, avec les mêmes vérifications. Les tentatives refusées et annulées permettent une nouvelle tentative, de sorte qu’une commande impayée reste open : libérez-la selon votre propre calendrier, et uniquement tant que son paymentStatus n’est pas pending. Une requête en échec ou un dépassement de délai n’est pas un refus : gardez donc la commande réservée jusqu’à ce que le paiement ait un résultat connu.

Ajouter Apple Pay et Google Pay

Les portefeuilles apparaissent dans la frame de Tapaya, à côté des champs de carte. Avant qu’ils ne s’affichent, vérifiez que :

  • Les portefeuilles sont activés dans les paramètres du Checkout.
  • Apple Pay : la boutique dispose d’un domaine de boutique actif.
  • Google Pay en production : un Google Pay merchant ID est configuré.
  • L’appareil, le navigateur et le portefeuille du client prennent en charge ce moyen de paiement.

Transmettez wallets avec le total du checkout en unités mineures. Tapaya affiche les moyens disponibles, et les portefeuilles non pris en charge ou désactivés restent masqués. Utilisez ceci à la place de l’exemple createPayment() limité aux cartes :

checkout-wallets.ts
const payment = tapaya.createPayment({
	prepare: '/api/payments',
	wallets: { amount: order.totalInMinorUnits, currency: order.currency },
	onWalletClick: async () => {
		if (!form.reportValidity()) throw new Error('Complete the checkout details.')
		// Validate the cart and reserve the order on your server.
		const response = await fetch('/api/payments', { method: 'POST' })
		if (!response.ok) throw new Error('Could not prepare the payment.')
		return response.json() // { clientSecret, status }
	},
	onChange: (snapshot) => {
		button.hidden = snapshot.paymentMethod !== 'card'
		button.disabled = !snapshot.canSubmit
		if (snapshot.outcome === 'completed') location.assign('/checkout/confirmation')
	},
})
await payment.mount('#card-fields')

Le total affiché dans le navigateur doit provenir de votre serveur, qui calcule et valide tout de même le montant lorsqu’il prépare le paiement.

Clics sur un portefeuille

Le bouton officiel du portefeuille ouvre la fenêtre de paiement native du portefeuille à partir du clic du client. onWalletClick valide votre checkout et réserve son paiement lorsque ce bouton est cliqué, jamais au montage ni lors de la sélection du moyen de paiement. Sans ce callback, les portefeuilles utilisent prepare ou clientSecret. Si vous ne réservez la commande qu’au clic, faites en sorte que le callback prepare appelé au montage reprenne une commande existante sans en créer une.

N’affichez votre propre bouton de paiement que lorsque snapshot.paymentMethod vaut card ; les portefeuilles utilisent leurs boutons de marque au lieu de payment.submit(). Les résultats des portefeuilles suivent le même flux de vérification et de récupération que les cartes. Fermer la fenêtre de paiement du portefeuille laisse le checkout disponible ; un paiement incertain reste en attente et bloque tout autre débit.

Mettre à jour le total

Transmettez wallets: { amount, currency, disabled: true } tant que les informations de checkout obligatoires sont incomplètes. Mettez à jour le total ou activez les boutons sans remonter le composant :

payment.update({ wallets: { amount: 2450, currency: 'EUR', disabled: false } })

Transmettez wallets: null pour masquer les portefeuilles jusqu’à ce que le total soit prêt. Tapaya refuse la confirmation si le paiement préparé diffère du total du portefeuille. Le hook React usePayment() accepte ces options et met à jour le total lorsque wallets change.

Votre propre sélecteur de moyen de paiement

Si votre page gère elle-même le sélecteur de moyen de paiement, transmettez paymentMethod: 'card', 'apple_pay' ou 'google_pay' pour n’afficher que ce moyen. Un portefeuille sélectionné mais indisponible affiche le libellé walletUnavailable et signale snapshot.error.code: 'wallet_unavailable' lorsqu’un autre paiement peut être démarré sans risque ; proposez alors un autre moyen dans votre sélecteur. Verrouillez le sélecteur tant que snapshot.busy est vrai ou que le paiement est pending ou action_required.

Frames et CSP

Le SDK ajoute allow="payment" à sa frame. Si votre boutique se trouve elle-même dans une autre frame, cette frame externe doit également autoriser le paiement. Apple Pay dans un contexte cross-origin nécessite Safari 17 ou version ultérieure. Les scripts des fournisseurs de portefeuille restent dans la frame de Tapaya : votre page n’a donc besoin d’aucune origine de fournisseur supplémentaire dans sa CSP.

Gérer l’expiration de l’accès navigateur

Le client secret expire au bout de 24 heures, et clientSecret vaut alors null. Un paiement qui n’a jamais été soumis est alors annulé, et le prepare() suivant démarre une nouvelle tentative pour la même commande. Un paiement en attente reste pending dans le navigateur, et le SDK continue d’appeler votre endpoint jusqu’à ce que votre serveur indique son résultat dans status. Les lectures côté serveur fonctionnent toujours : votre tâche planifiée le finalise donc dans tous les cas.

Diagnostiquer les erreurs serveur

Le SDK Node lève PaymentsApiError pour les requêtes d’API refusées. Examinez ses status, code, message, fieldErrors et requestId sur votre serveur. Tenez les identifiants et les client secrets à l’écart des journaux, et affichez vos propres textes destinés aux clients.

Adapter à votre image de marque

Les champs de carte utilisent la couleur Checkout effectivement appliquée au commerçant, définie dans les paramètres du Checkout, comme accent de focus (colorFocus). Les valeurs que vous transmettez dans appearance.variables la remplacent, et seules les variables que vous définissez sont envoyées.

Il manque quelque chose sur cette page ?Demandez une fonctionnalité. Nous joignons cette page pour que l’équipe ait le contexte.