# Embedded Checkout (/fr/online/integration/embedded_checkout)



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**.

<EmbeddedPaymentFlowDiagram />

<Callout title="Moyens de paiement" type="info">
  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](#add-apple-pay-and-google-pay).
</Callout>

## Avant de commencer [#before-you-start]

### Installer le SDK [#install-the-sdk]

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

<CodeBlockTabs defaultValue="npm" groupId="package-manager">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      Bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```sh
    npm install @tapayadot/checkout
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```sh
    pnpm add @tapayadot/checkout
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```sh
    bun add @tapayadot/checkout
    ```
  </CodeBlockTab>
</CodeBlockTabs>

| Import                       | Où                                                                                                                                   |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `@tapayadot/checkout`        | Votre 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/client` | Votre page de paiement.                                                                                                              |
| `@tapayadot/checkout/react`  | Votre page de paiement, si vous utilisez React.                                                                                      |

### Configurer votre serveur [#configure-your-server]

Les paiements intégrés n’ont pas besoin d’URL de retour. Les exemples serveur ci-dessous existent en versions &#x2A;*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 SDK                                                                                                                                                             | HTTP (fetch) et cURL                                                                                                       |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| **Clé**             | `TAPAYA_SECRET_KEY`, lue par `new Tapaya()`                                                                                                                             | `TAPAYA_CHECKOUT_API_KEY`                                                                                                  |
| **Environnement**   | `TAPAYA_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 plateforme** | Transmettez 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](/online/integration/api_integration#authentication). Les client secrets sont propres à chaque paiement.

Effectuez l’[onboarding](/merchants/onboarding) avec les paiements en ligne et vérifiez que le commerçant est **Ready** dans les
[paramètres du Checkout](/online/integration/checkout_settings).

### Autoriser Tapaya dans votre CSP [#allow-tapaya-in-your-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 :

```text
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 [#how-it-works]

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 [#accept-a-payment]

<div className="fd-steps [&_h3]:fd-step">
  ### Obtenir les paramètres du navigateur [#get-the-browser-settings]

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

  <Tabs defaultValue="fetch" groupId="checkout-server" className="border-0 bg-transparent">
    <TabsList>
      <TabsTrigger value="fetch">
        HTTP (fetch)
      </TabsTrigger>

      <TabsTrigger value="node">
        Node.js SDK
      </TabsTrigger>

      <TabsTrigger value="curl">
        cURL
      </TabsTrigger>
    </TabsList>

    <TabsContent value="fetch">
      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.

      ```js title="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.
    </TabsContent>

    <TabsContent value="node">
      ```ts title="server/config.ts"
      import { Tapaya } from '@tapayadot/checkout'

      const tapaya = new Tapaya()
      const { publishableKey, apiOrigin } = await tapaya.checkout.configuration()
      // Sandbox: publishableKey: 'pk_test_…', apiOrigin: 'https://api.sandbox.tapaya.com'
      // Production: publishableKey: 'pk_live_…', apiOrigin: 'https://api.tapaya.com'
      ```
    </TabsContent>

    <TabsContent value="curl">
      Exécutez ceci sur votre serveur ou dans un terminal de confiance. En mode plateforme, ajoutez
      `--header "X-Tapaya-Merchant-Token: $TAPAYA_MERCHANT_TOKEN"` à chaque commande cURL de ce guide.

      ```bash
      curl "$TAPAYA_CHECKOUT_API_URL/merchant/payments/configuration" \
      	--fail-with-body --silent --show-error --max-time 35 \
      	--header "Authorization: Bearer $TAPAYA_CHECKOUT_API_KEY"
      ```

      Utilisez la `publishableKey` renvoyée et l’origine de votre `TAPAYA_CHECKOUT_API_URL` comme configuration du navigateur.
    </TabsContent>
  </Tabs>

  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 [#add-the-card-fields]

  Ajoutez un conteneur à votre formulaire de paiement existant :

  ```html title="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.

  ```ts title="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 [#return-the-orders-payment]

  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.

  <Tabs defaultValue="fetch" groupId="checkout-server" className="border-0 bg-transparent">
    <TabsList>
      <TabsTrigger value="fetch">
        HTTP (fetch)
      </TabsTrigger>

      <TabsTrigger value="node">
        Node.js SDK
      </TabsTrigger>

      <TabsTrigger value="curl">
        cURL
      </TabsTrigger>
    </TabsList>

    <TabsContent value="fetch">
      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`.

      ```js title="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.
    </TabsContent>

    <TabsContent value="node">
      ```ts title="server/payments.ts"
      import { Tapaya } from '@tapayadot/checkout'
      import type { PreparedPayment } from '@tapayadot/checkout'
      import { getCheckoutOrder } from './orders'

      const tapaya = new Tapaya()

      export async function POST(request: Request) {
      	const order = await getCheckoutOrder(request)
      	const details = { merchantOrderId: order.id, amount: order.amount, currency: order.currency }
      	// Only an order awaiting payment gets a payable session. Other orders report their payment's outcome.
      	const payable = order.status === 'pending_payment'
      	const session = payable
      		? await tapaya.checkout.sessions.prepare(details)
      		: await tapaya.checkout.sessions.verify(details)
      	if (!session) return Response.json({ error: 'Order cannot accept payment' }, { status: 409 })
      	// The card fields read both values. Your page code never handles them.
      	return Response.json(
      		{ clientSecret: payable ? session.clientSecret : null, status: session.status } satisfies PreparedPayment,
      		{ headers: { 'Cache-Control': 'no-store' } },
      	)
      }
      ```

      `prepare()` renvoie le paiement de la commande. Il crée la première tentative, renvoie sans la modifier une tentative non résolue, et
      crée la tentative suivante après un refus. Il dérive lui-même les idempotency keys : l’appeler à chaque chargement de page et à chaque
      soumission ne crée donc jamais de second débit. Figez le total dès que le paiement commence : `prepare()` lève
      `TapayaOrderMismatchError` si le montant ou la devise de la commande ne correspondent plus à son paiement.
    </TabsContent>

    <TabsContent value="curl">
      Recherchez d’abord le paiement de la commande. Ces exemples utilisent une commande de 121,00 EUR enregistrée sur votre serveur. Un `404` signifie
      qu’aucun paiement n’a été trouvé ; les autres échecs HTTP n’autorisent pas la création d’une autre tentative.

      ```bash
      curl --get "$TAPAYA_CHECKOUT_API_URL/merchant/payments" \
      	--data-urlencode 'merchantOrderId=order-1001' \
      	--fail-with-body --silent --show-error --max-time 35 \
      	--header "Authorization: Bearer $TAPAYA_CHECKOUT_API_KEY"
      ```

      S’il n’existe aucun paiement et que la commande peut encore accepter un paiement, créez la première tentative :

      ```bash
      curl --request POST "$TAPAYA_CHECKOUT_API_URL/merchant/payments" \
      	--fail-with-body --silent --show-error --max-time 35 \
      	--header "Authorization: Bearer $TAPAYA_CHECKOUT_API_KEY" \
      	--header 'Content-Type: application/json' \
      	--header 'Idempotency-Key: order-1001:initial' \
      	--data '{"merchantOrderId":"order-1001","amount":12100,"currency":"EUR"}'
      ```

      Réutilisez un paiement non résolu existant. Après `declined` ou `cancelled`, ne créez une autre tentative que si
      `retryAllowed` vaut `true`. Pour cette tentative, répétez le POST avec les mêmes informations de commande et la clé
      `order-1001:after:<previous-payment-id>`, en remplaçant l’espace réservé par l’`id` renvoyé. Réutilisez cette clé pour
      les nouvelles tentatives. Après un dépassement de délai, recherchez à nouveau la commande ; ne supposez pas que le POST a échoué.

      cURL illustre les appels d’API. Le gestionnaire authentifié `POST /api/payments` de votre application doit renvoyer
      `{ clientSecret, status }` aux champs de carte. Convertissez le statut de l’API à l’aide du tableau ci-dessous, et envoyez
      `clientSecret: null` lorsque votre commande n’accepte plus de paiement. Ne placez jamais ces réponses dans des journaux de terminal partagés.
    </TabsContent>
  </Tabs>

  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’API                                                                                      | `status` de l’endpoint prepare |
  | ------------------------------------------------------------------------------------------------------ | ------------------------------ |
  | `successful`                                                                                           | `completed`                    |
  | `declined` ou `cancelled`, avec `retryAllowed: false`                                                  | `ended`                        |
  | `ready`, `pending`, `action_required`, ou un refus ou une annulation permettant une nouvelle tentative | `open`                         |

  <Accordions>
    <Accordion title="Règles pour votre endpoint prepare">
      * **Prenez le montant dans votre commande enregistrée,** en unités mineures entières, jamais depuis le navigateur. Figez le total dès que
        le paiement commence.
      * **Répondez aussi pour les commandes finalisées.** Une commande déjà payée, ou que votre tâche planifiée a finalisée, répond toujours
        avec le `status` de son paiement, afin qu’un rechargement affiche le résultat au lieu d’une erreur. Elle ne reçoit jamais de client secret,
        de sorte que le client ne peut pas payer une commande pour laquelle vous n’acceptez plus de paiement.
      * **Un seul paiement réussi par commande.** Chaque refus définit `retryAllowed`, afin que le client puisse essayer une autre carte pour
        la même commande. Cette limite est partagée avec le [Hosted Checkout](/online/integration/api_integration#one-payment-per-order).
      * **Protégez le client secret.** Envoyez-le uniquement au client autorisé à payer cette commande, et ne le journalisez jamais.
      * **Attendez-vous à un POST minimal.** Les champs de carte envoient un POST à `prepare` sur votre propre origine, avec les cookies et sans corps. Ils refusent
        les redirections et ne relancent jamais automatiquement la requête. Pour des en-têtes personnalisés, des jetons CSRF ou un corps de requête, transmettez une
        fonction qui renvoie le même JSON au lieu de l’URL : `prepare: async () => (await fetch(…)).json()`.
    </Accordion>
  </Accordions>

  ### Soumettre depuis votre gestionnaire [#submit-from-your-handler]

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

  ```ts title="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 [#show-the-outcome]

  `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` :

  | `outcome`   | Ce qu’il faut afficher                                                                                                                                                          |
  | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `open`      | Le bouton de paiement, activé tant que `canSubmit` est vrai. Un refus reste `open`, et le `submit()` suivant obtient une nouvelle tentative.                                    |
  | `pending`   | Un 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. |
  | `completed` | Votre page de confirmation. Votre serveur y vérifie la commande.                                                                                                                |
  | `ended`     | Un 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 [#verify-before-fulfilling]

  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é :

  <Tabs defaultValue="fetch" groupId="checkout-server" className="border-0 bg-transparent">
    <TabsList>
      <TabsTrigger value="fetch">
        HTTP (fetch)
      </TabsTrigger>

      <TabsTrigger value="node">
        Node.js SDK
      </TabsTrigger>

      <TabsTrigger value="curl">
        cURL
      </TabsTrigger>
    </TabsList>

    <TabsContent value="fetch">
      ```js title="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);
      ```
    </TabsContent>

    <TabsContent value="node">
      ```ts title="server/confirmation.ts"
      const order = await getCheckoutOrder(request)
      const session = await tapaya.checkout.sessions.verify({
      	merchantOrderId: order.id,
      	amount: order.amount,
      	currency: order.currency,
      })
      if (session?.status === 'completed') await markOrderPaid(order.id, session.id)
      ```

      `verify()` ne crée jamais de paiement et renvoie `null` avant la première tentative. Il lève
      `TapayaOrderMismatchError` lorsque le montant ou la devise diffèrent de votre commande.
    </TabsContent>

    <TabsContent value="curl">
      Récupérez le paiement actuel de la commande une fois que le navigateur a terminé, ou lorsque vous vérifiez un résultat incertain :

      ```bash
      curl --get "$TAPAYA_CHECKOUT_API_URL/merchant/payments" \
      	--data-urlencode 'merchantOrderId=order-1001' \
      	--fail-with-body --silent --show-error --max-time 35 \
      	--header "Authorization: Bearer $TAPAYA_CHECKOUT_API_KEY"
      ```

      Avant de traiter la commande d’exemple, vérifiez `merchantOrderId: "order-1001"`, `amount: 12100`, `currency: "EUR"`
      et `status: "successful"`. Enregistrez l’`id` de paiement renvoyé avec votre commande. Une réponse HTTP 200 seule ne prouve pas le paiement.
    </TabsContent>
  </Tabs>

  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é.

  <Callout title="Rapprochez les commandes dont les clients partent" type="warn">
    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.
  </Callout>
</div>

## Ajouter Apple Pay et Google Pay [#add-apple-pay-and-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](/online/integration/checkout_settings#wallets-and-appearance).
* **Apple Pay :** la boutique dispose d’un [domaine de boutique actif](/online/integration/checkout_settings#register-apple-pay-shop-domains).
* **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 :

```ts title="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 [#wallet-clicks]

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 [#update-the-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 :

```ts
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 [#your-own-method-selector]

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 [#frames-and-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 [#handle-expired-browser-access]

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 [#diagnose-server-errors]

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 [#match-your-branding]

Les champs de carte utilisent la couleur Checkout effectivement appliquée au commerçant, définie dans les [paramètres du
Checkout](/online/integration/checkout_settings#wallets-and-appearance), 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.
