# Application Tapaya Terminal (/fr/in-store/integration/card_terminal)



Acceptez les paiements par carte sans contact depuis votre application Android en confiant l’interaction avec la carte à l’application plugin
**Tapaya Terminal**. Votre application garde le contrôle de la vente ; le plugin gère la présentation sans contact via NFC, la saisie du PIN et le
traitement de la carte, puis le résultat revient par le flux d’événements de `Accept.payments.pay()`.

Utilisez cette intégration si vous voulez Tap to Pay sans intégrer de processeur de paiement dans votre propre application. Si votre
plateforme est plutôt activée pour Tap to Pay intégré, consultez le guide *Embedded Tap to Pay*.

## Options d’intégration [#integration-options]

| Option                      | Idéal pour                                      | Remise du résultat                                                                                                        |
| :-------------------------- | :---------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ |
| **Accept SDK** (recommandé) | Applications Android natives                    | `Accept.payments.pay()` émet un `Flow<PaymentEvent>` : création, lancement et résultat final du terminal, gérés pour vous |
| **Deeplink**                | WebViews, frameworks multiplateformes, QR codes | Vous créez le paiement via l’API Tapaya et interrogez régulièrement son statut ; le plugin s’ouvre via un deeplink        |

Le parcours via le SDK est décrit en premier ; consultez [Intégrer sans le SDK](#integrate-without-the-sdk) pour le contrat
du deeplink.

## Fonctionnement [#how-it-works]

1. Votre application démarre un paiement avec `Accept.payments.pay(amount, currency)`.
2. Le SDK enregistre le paiement auprès du backend Tapaya et lance le plugin Tapaya Terminal sur le même appareil.
3. Le client approche sa carte ou son téléphone de l’écran de Tapaya Terminal.
4. Tapaya Terminal finalise le paiement avec le backend Tapaya.
5. Le SDK suit le paiement jusqu’à ce qu’il atteigne un état final et émet `PaymentEvent.Result` contenant un
   `PayResult`. Si le paiement ne peut pas du tout être créé, il émet à la place `PaymentEvent.CreationFailed`, généralement
   en moins d’une seconde.

## Prérequis [#prerequisites]

* Le **plugin Tapaya Terminal** installé sur l’appareil et **activé** pour votre commerçant (voir
  [Installer et activer le terminal](#install-and-activate-the-terminal)).
* Un appareil équipé du NFC, sous Android 11 (API 30) ou version ultérieure.
* L’Accept SDK [installé et initialisé](../integration/sdk_integration#initialization), avec le commerçant
  [connecté](../integration/sdk_integration#step-2-authenticate).
* L’autorisation de localisation (`ACCESS_FINE_LOCATION`) accordée à votre application à l’exécution.

## Installer et activer le terminal [#install-and-activate-the-terminal]

Effectuez l’activation une fois par appareil. L’activation associe le plugin Tapaya Terminal au même compte commerçant que celui
avec lequel votre application est connectée.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    if (!Accept.plugin.isInstalled()) {
        // Opens the store listing so the user can install Tapaya Terminal
        Accept.plugin.install()
        return
    }

    // suspend; launches the plugin's activation flow and waits for the outcome
    when (val result = Accept.plugin.activateTerminal()) {
        ActivateTerminalResult.Success -> { /* device linked, ready to take payments */ }
        ActivateTerminalResult.Canceled -> { /* user backed out */ }
        is ActivateTerminalResult.Failed -> result.reason // ActivateTerminalFailureReason
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    if (!Accept.plugin.isInstalled()) {
        // Opens the store listing so the user can install Tapaya Terminal
        await Accept.plugin.install();
        return;
    }

    // launches the plugin's activation flow and waits for the outcome
    const result = await Accept.plugin.activateTerminal();
    switch (result.type) {
        case 'success': /* device linked, ready to take payments */ break;
        case 'canceled': /* user backed out */ break;
        case 'failed': handle(result.reason); break; // ActivateTerminalFailureReason
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

`ActivateTerminalFailureReason` prend l’une des valeurs `INVALID_INTENT`, `HANDOVER_FAILED`, `CONFIRM_FAILED`,
`MERCHANT_NOT_ONBOARDED` ou `UNKNOWN`.

<Callout title="Remarque" type="info">
  `Accept.plugin.isInstalled()` renvoie `false` lorsque le plugin Tapaya Terminal est absent. Lisez à tout moment
  l’état actuel du plugin avec `Accept.plugin.status()`, et conditionnez l’accès à votre interface de paiement en conséquence.
</Callout>

`activateTerminal()` **lève** également `NoTidsAvailableForMerchant` lorsque le commerçant n’a plus aucun terminal non attribué
(TID) à activer, indépendamment des résultats `ActivateTerminalResult.Failed` ci-dessus. Encadrez l’appel d’un
`try-catch` si vous souhaitez traiter ce cas à part, par exemple en invitant le commerçant à libérer ou à
demander un autre TID.

### Vérifier les mises à jour du plugin [#checking-for-plugin-updates]

`Accept.plugin.updateInfo()` indique si une version plus récente du plugin Tapaya Terminal est disponible sur Google
Play, afin que vous puissiez inviter le commerçant à le mettre à jour avant qu’un paiement ne se heurte à un écart de version.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    when (val info = Accept.plugin.updateInfo()) {
        PluginUpdateInfo.UpToDate -> { /* nothing to do */ }
        is PluginUpdateInfo.UpdateAvailable -> promptUpdate(info.priority) // Int, higher is more urgent
        PluginUpdateInfo.Unknown -> { /* not installed, or an older plugin build that predates this check */ }
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    const info = await Accept.plugin.updateInfo();
    switch (info.type) {
        case 'upToDate': /* nothing to do */ break;
        case 'updateAvailable': promptUpdate(info.priority); break; // higher is more urgent
        case 'unknown': /* not installed, or a plugin build that predates this check */ break;
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<Callout title="Remarque" type="info">
  Le SDK n’interroge jamais Google Play directement ; l’application plugin effectue elle-même la vérification via Play Core et
  transmet le résultat par son canal de statut existant. Une version du plugin antérieure à celle qui a introduit cette fonctionnalité renvoie toujours
  `Unknown`.
</Callout>

## Créer un paiement [#create-a-payment]

Les montants sont toujours exprimés dans la **plus petite unité de la devise** : les hellers pour CZK, les centimes pour EUR. `1500` avec
la devise `"CZK"` débite 15,00 Kč. Les devises sont des codes ISO 4217.

`pay()` renvoie un `Flow<PaymentEvent>` ; collectez-le pour suivre la progression et le résultat final. Il ne lève jamais d’exception ;
les échecs se manifestent sous la forme de `PaymentEvent.CreationFailed`.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    Accept.payments.pay(amount = 1500, currency = "CZK") // 15.00 Kč
        .collect { event ->
            when (event) {
                PaymentEvent.Creating -> { /* registering the payment */ }
                is PaymentEvent.Created -> { /* backend payment created: event.paymentToken */ }
                PaymentEvent.Launched -> { /* Tapaya Terminal is in the foreground */ }
                is PaymentEvent.Result -> when (val r = event.payResult) {
                    is PayResult.Success -> showReceipt(r.paymentToken, r.authCode)
                    is PayResult.Declined -> showDeclined(r.metadata)
                    is PayResult.Canceled -> showCancelled(r.metadata)
                    is PayResult.Failed -> showUnexpected(r.reason) // PayFailureReason
                }
                is PaymentEvent.CreationFailed -> handleError(event.cause) // AcceptException
            }
        }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    Accept.payments.pay({ amount: 1500, currency: 'CZK' }, (event) => { // 15.00 Kč
        switch (event.type) {
            case 'creating': /* registering the payment */ break;
            case 'created': /* backend payment created: event.paymentToken */ break;
            case 'launched': /* Tapaya Terminal is in the foreground */ break;
            case 'result':
                switch (event.payResult.type) {
                    case 'success': showReceipt(event.payResult.paymentToken, event.payResult.authCode); break;
                    case 'declined': showDeclined(event.payResult.metadata); break;
                    case 'canceled': showCancelled(event.payResult.metadata); break;
                    case 'failed': showUnexpected(event.payResult.reason); break; // PayFailureReason
                }
                break;
            case 'creationFailed': handleError(event.error); break; // AcceptError payload
        }
    });
    ```
  </CodeBlockTab>
</CodeBlockTabs>

**Paramètres :**

| Paramètre            | Type                    | Obligatoire | Description                                                                                                                                                                                                                                                                                                        |
| :------------------- | :---------------------- | :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `amount`             | `Long`                  | ✓           | Montant à débiter, dans l’unité mineure de `currency` (par exemple, les cents pour USD, les hellers pour CZK).                                                                                                                                                                                                     |
| `currency`           | `String`                | ✓           | Code de devise ISO 4217.                                                                                                                                                                                                                                                                                           |
| `paymentToken`       | `String?`               |             | Reprend une session de paiement existante au lieu d’en démarrer une nouvelle. Vaut `null` par défaut.                                                                                                                                                                                                              |
| `receiptConfig`      | `PaymentReceiptConfig?` |             | Remplace l’écran de reçu affiché après un paiement **approuvé**, pour cet appel uniquement. Vaut `null` par défaut, auquel cas la valeur par défaut définie avec `Accept.payments.setOptions()` s’applique.                                                                                                        |
| `nfcPosition`        | `NfcPositionConfig?`    |             | Remplace l’endroit vers lequel pointe l’animation de paiement sans contact, pour cet appel uniquement. Vaut `null` par défaut, auquel cas la valeur par défaut définie avec `Accept.payments.setOptions()` s’applique.                                                                                             |
| `metadata`           | `Map<String, String>`   |             | Données clé-valeur associées au paiement. Envoyées au plugin Tapaya Terminal dans l’intent `PAY` et renvoyées dans le `PayResult` qui en résulte, si le plugin les renvoie. Vaut `emptyMap()` par défaut.                                                                                                          |
| `tip`                | `Long?`                 |             | Pourboire fixe, dans l’unité mineure de `currency`. S’il est défini, le plugin ignore son propre écran interactif de pourboire et débite `amount + tip`. Vaut `null` par défaut (le plugin demande un pourboire au payeur).                                                                                        |
| `transactionTimeout` | `Duration?`             |             | Remplace la durée pendant laquelle le terminal attend la présentation d’une carte, pour cet appel uniquement. Vaut `null` par défaut, auquel cas la valeur par défaut définie avec `Accept.payments.setOptions()` s’applique. Voir [Définir un délai d’expiration de transaction](#setting-a-transaction-timeout). |
| `display`            | `PaymentDisplay?`       |             | Remplace l’écran sur lequel s’exécute l’interface de paiement du plugin, pour cet appel uniquement. Vaut `null` par défaut, auquel cas la valeur par défaut définie avec `Accept.payments.setOptions()` s’applique. Voir [Cibler un écran](#targeting-a-display).                                                  |

Pendant le paiement, le plugin Tapaya Terminal est au premier plan. Votre application reprend automatiquement
à la fin du paiement. Pour reprendre un paiement interrompu, repassez son `paymentToken` à
`pay(amount, currency, paymentToken)`.

<Callout title="Le renvoi de metadata n’est pas garanti" type="info">
  `metadata` n’est renvoyé dans `PayResult` que si le plugin le renvoie ; considérez-le donc comme un contexte sans garantie
  (par exemple, un ID de commande pour votre propre journalisation), et non comme des données que votre application doit impérativement recevoir en retour.
</Callout>

### Choisir la devise [#choosing-the-currency]

`pay()` débite dans la devise `currency` que vous transmettez, sous forme de code ISO 4217 en majuscules, avec `amount` dans l’unité
mineure de cette devise. La page [Devises prises en charge](/merchants/supported_currencies) liste toutes les devises prises en charge par Tapaya, mais un commerçant
n’est généralement activé que pour certaines d’entre elles.

Appelez `Accept.merchant.availableCurrencies()` pour obtenir les codes ISO 4217 pour lesquels le commerçant est activé. Un paiement dans toute autre
devise échoue avec `CurrencyNotAvailableForMerchant`.

Chaque devise a également un montant minimum payable, appliqué à l’ensemble de la plateforme. Récupérez les minimums actuels avec
`Accept.sdk.minimumAmounts()`, qui renvoie une `List<MinimumAmount>` (`currency: String, amount: Long`, dans l’unité
mineure de la devise). Un appel à `pay()` en dessous de ce minimum échoue avec
`PaymentEvent.CreationFailed(AmountBelowMinimum(minimum, currency))`. Consultez
[Gestion des erreurs](./sdk_integration#error-handling) pour la liste complète des exceptions.

### Configurer l’écran de reçu [#configuring-the-receipt-screen]

`receiptConfig` détermine ce que le plugin Tapaya Terminal affiche au payeur après un paiement **approuvé**, combien de temps
cela reste à l’écran et si le reçu est imprimé. Ce paramètre n’a aucun effet sur les résultats refusés, annulés ou en attente
d’annulation ; ceux-ci affichent toujours leur propre écran de résultat, quel que soit ce paramètre.

| `PaymentReceiptConfig`                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DismissImmediately`                                      | Rien n’est affiché au payeur.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `Show(showQrCodeReceipt, dismissBehavior, printBehavior)` | Affiche l’écran de reçu. `showQrCodeReceipt: Boolean = true` active ou désactive un QR code menant au reçu. `dismissBehavior: DismissBehavior = DismissBehavior.Manual` détermine la façon dont l’écran se ferme : `Manual` (reste affiché jusqu’à ce que le payeur le ferme) ou `Delayed(delay: Duration)` (se ferme automatiquement après `delay` si le payeur ne l’a pas fermé avant). `printBehavior: ReceiptPrintBehavior = ReceiptPrintBehavior.OnDemand` contrôle le ticket papier : `Automatic` (imprimé dès l’affichage de l’écran), `OnDemand` (uniquement lorsque le caissier le déclenche) ou `Disabled` (jamais imprimé). |

Définissez une valeur par défaut pour tout l’appareil une fois avec `Accept.payments.setOptions()`, et remplacez-la pour un paiement donné via
`pay(..., receiptConfig = ...)`. Tant que `setOptions()` n’a pas été appelé, la valeur par défaut est `Show` avec un reçu QR, une fermeture
manuelle et une impression à la demande, ce qui correspond au comportement de repli du plugin lui-même.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    // Set a default, e.g. once at app startup
    Accept.payments.setOptions(
        receiptConfig = PaymentReceiptConfig.Show(
            showQrCodeReceipt = true,
            dismissBehavior = DismissBehavior.Delayed(30.seconds),
            printBehavior = ReceiptPrintBehavior.Automatic,
        ),
    )

    // Override it for a single payment
    Accept.payments.pay(
        amount = 1500,
        currency = "CZK",
        receiptConfig = PaymentReceiptConfig.DismissImmediately,
    )
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    // Set a default, e.g. once at app startup
    Accept.payments.setOptions({
        receiptConfig: {
            type: 'show',
            showQrCodeReceipt: true,
            dismissBehavior: { type: 'delayed', delayMs: 30_000 },
            printBehavior: 'automatic',
        },
    });

    // Override it for a single payment
    await Accept.payments.payAsync({
        amount: 1500,
        currency: 'CZK',
        receiptConfig: { type: 'dismissImmediately' },
    });
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Positionner l’animation de paiement sans contact NFC [#positioning-the-nfc-tap-animation]

`nfcPosition` est une indication qui précise au plugin Tapaya Terminal où se trouve l’antenne NFC de cet appareil, afin qu’il puisse placer
correctement l’animation « approchez ici ». Le plugin détermine d’abord lui-même la position de l’antenne (détection en direct par la plateforme,
puis base de données d’appareils intégrée, puis tout ce que le commerçant a configuré directement dans le plugin) ; ne fournissez cette valeur
que pour remplacer cette détermination, par exemple sur un appareil que le plugin ne sait pas détecter ou qui est mal renseigné dans sa base de données.

| `NfcPositionConfig`                       | Description                                                                                                                                                                     |
| :---------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Unspecified`                             | Aucun remplacement (par défaut). Le plugin détermine lui-même la position et conserve toute position que le commerçant y a déjà configurée.                                     |
| `OnDevice(surface, xFraction, yFraction)` | L’antenne se trouve sur l’appareil lui-même ; l’animation pointe vers `(xFraction, yFraction)` sur `surface`, soit `OnDeviceSurface.BACK`, soit `OnDeviceSurface.FRONT_SCREEN`. |
| `ExternalAbove`                           | Un module lecteur distinct se trouve au-dessus du téléphone (support smart-POS) ; il n’y a aucun point à placer sur l’appareil.                                                 |

Définissez une valeur par défaut pour tout l’appareil avec `Accept.payments.setOptions(nfcPosition = ...)`, ou remplacez-la pour un appel donné via
`pay(..., nfcPosition = ...)`. `setOptions()` ne modifie que la ou les options que vous transmettez ; ainsi, `setOptions(nfcPosition = ...)`
seul ne touche pas à une valeur par défaut de `receiptConfig` définie auparavant, et inversement.

### Définir un délai d’expiration de transaction [#setting-a-transaction-timeout]

`transactionTimeout` détermine combien de temps le plugin Tapaya Terminal attend que le client présente sa carte avant
d’abandonner. Il accepte toute `Duration` comprise entre zéro et 120 secondes ; toute valeur hors de cette plage se manifeste sous la forme de
`TransactionTimeoutOutOfRange(transactionTimeout)` via `PaymentEvent.CreationFailed`, puisque `pay()` ne lève jamais d’exception
de manière synchrone.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    Accept.payments.pay(
        amount = 1500,
        currency = "CZK",
        transactionTimeout = 90.seconds,
    )
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    await Accept.payments.payAsync({
        amount: 1500,
        currency: 'CZK',
        transactionTimeoutMs: 90_000, // milliseconds, 0…120000
    });
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Laissez la valeur par défaut `null` pour utiliser le délai de repli de l’application compagnon, d’environ 60 secondes, ou définissez une valeur
par défaut pour tout l’appareil avec `Accept.payments.setOptions(transactionTimeout = ...)`.

### Cibler un écran [#targeting-a-display]

Sur un appareil à double face (« double-sided »), un panneau est tourné vers le commerçant et un second panneau, côté client, est tourné vers
le payeur. `Accept.displays` indique les écrans dont dispose l’appareil à l’instant présent, et l’option `display` indique au plugin
sur quel panneau exécuter son interface de paiement.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    if (Accept.displays.isDualScreen) {
        val customerPanel = Accept.displays.customerFacing() // AcceptDisplay?, null on single-screen devices
    }

    Accept.payments.pay(
        amount = 1500,
        currency = "CZK",
        display = PaymentDisplay.CustomerFacing,
    )
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    if (Accept.displays.isDualScreen) {
        const customerPanel = Accept.displays.customerFacing(); // null on single-screen devices
    }

    await Accept.payments.payAsync({
        amount: 1500,
        currency: 'CZK',
        display: { type: 'customerFacing' },
    });
    ```
  </CodeBlockTab>
</CodeBlockTabs>

| `PaymentDisplay`      | Description                                                                                                |
| :-------------------- | :--------------------------------------------------------------------------------------------------------- |
| `Default`             | L’écran actuel de l’application hôte (comportement par défaut, inchangé sur les appareils à écran unique). |
| `CustomerFacing`      | Le panneau secondaire d’un appareil à double face.                                                         |
| `Specific(displayId)` | Un écran explicite, désigné par l’`id` indiqué dans `Accept.displays.all()`.                               |

Une cible qui ne peut pas être respectée, par exemple `CustomerFacing` sur un appareil à écran unique, se replie silencieusement sur
`Default`. Définissez une valeur par défaut pour tout l’appareil avec `Accept.payments.setOptions(display = ...)`, ou remplacez-la pour un appel donné via
`pay(..., display = ...)`.

<Callout title="Définissez la cible avant l’authentification" type="info">
  La préparation du terminal (warm-up) déclenchée par `Accept.auth.authenticate()` suit la même cible ; appelez donc
  `setOptions(display = ...)` avant `authenticate()` si vous voulez que la préparation elle-même ne s’affiche pas sur
  l’écran du commerçant.
</Callout>

L’application d’exemple `:dualscreen` illustre un encaissement complet sur deux panneaux : le commerçant travaille sur le panneau principal tandis que
`PaymentDisplay.CustomerFacing` place l’interface de paiement du plugin sur l’écran du client.

## Résultat du paiement [#payment-outcome]

L’événement final `PaymentEvent.Result` contient un `PayResult`. Chaque variante contient `metadata`, qui renvoie ce que vous
avez transmis à `pay()` si le plugin le renvoie (peut valoir `null`) :

| `PayResult`                                                 | Signification                                                                                                                                                                                         |
| :---------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Success(paymentToken, metadata, authCode, receiptDetails)` | Autorisé et capturé. Les fonds seront réglés au commerçant. `authCode` est le code d’autorisation du processeur transmis par le terminal ; il est informatif et peut être absent selon le processeur. |
| `Declined(metadata, receiptDetails)`                        | Refusé par le processeur de paiement ou la banque émettrice.                                                                                                                                          |
| `Canceled(metadata)`                                        | Annulé par le client ou le commerçant avant la fin.                                                                                                                                                   |
| `Failed(reason, metadata)`                                  | Non abouti ; `reason` est un `PayFailureReason`.                                                                                                                                                      |

`PayFailureReason` prend l’une des valeurs `INVALID_INTENT`, `NOT_ACTIVATED`, `TERMINAL_INIT_FAILED`, `PAYMENT_SETUP_FAILED`,
`PLUGIN_UNAVAILABLE` ou `UNKNOWN`.

### Lire les détails du reçu [#reading-receipt-details]

`Success` et `Declined` contiennent un `receiptDetails: ReceiptDetails?` facultatif avec des données structurées de ticket carte
transmises par le terminal, destinées aux appelants qui impriment leur propre ticket carte officiel au lieu de s’appuyer uniquement sur
la page hébergée de `PaymentStatus.receiptUrl`.

| Champ                  | Type      | Description                                         |
| :--------------------- | :-------- | :-------------------------------------------------- |
| `merchantId`           | `String?` | ID du commerçant transmis par le processeur.        |
| `terminalId`           | `String?` | ID du terminal transmis par le processeur.          |
| `cardBrand`            | `String?` | Marque de la carte, par exemple Visa ou Mastercard. |
| `appLabel`             | `String?` | Libellé de l’application EMV.                       |
| `aid`                  | `String?` | Identifiant de l’application EMV.                   |
| `maskedPan`            | `String?` | Numéro de carte masqué.                             |
| `responseCode`         | `String?` | Code de réponse du processeur.                      |
| `transactionTimestamp` | `String?` | Horodatage de la transaction au format ISO-8601.    |

Chaque champ est facultatif et fourni sans garantie ; un appelant doit être prêt à ce que n’importe lequel d’entre eux, voire `receiptDetails` lui-même,
soit absent.

### Lire les détails du paiement [#reading-payment-details]

`PayResult.Success` vous fournit le `paymentToken`. Récupérez l’enregistrement complet, y compris `receiptUrl`, le montant demandé, le montant de
règlement ainsi que les horodatages, avec `Accept.payments.status()` :

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    val status: PaymentStatus = Accept.payments.status(paymentToken)
    status.state       // PENDING, SUCCESS, CANCELLED, REFUNDED, FAILED, ACTION_NEEDED
    status.receiptUrl  // for your receipt UI
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    const status = await Accept.payments.status(paymentToken);
    status.state;      // PENDING, SUCCESS, CANCELLED, REFUNDED, FAILED, ACTION_NEEDED
    status.receiptUrl; // for your receipt UI
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Arrêtez un paiement en cours avec `Accept.payments.cancel(paymentToken)`, qui renvoie le `PaymentState` qui en résulte.

### Effectuer un remboursement [#issuing-a-refund]

Remboursez un paiement finalisé avec `Accept.payments.refund(paymentToken, reason)`, qui renvoie lui aussi le
`PaymentState` qui en résulte.

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    val state: PaymentState = Accept.payments.refund(paymentToken, RefundReason.CUSTOMER_REQUEST)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    const state = await Accept.payments.refund(paymentToken, 'CUSTOMER_REQUEST');
    ```
  </CodeBlockTab>
</CodeBlockTabs>

`RefundReason` prend l’une des valeurs `CUSTOMER_REQUEST`, `DUPLICATE`, `FRAUDULENT` ou `EXPIRED_CHARGE`.

## Gestion des erreurs [#error-handling]

Un paiement qui atteint le terminal mais n’aboutit pas **n’est pas** une exception ; il arrive sous la forme de
`PaymentEvent.Result` contenant `PayResult.Declined`, `Canceled` ou `Failed(reason)`. Les échecs qui empêchent
la création du paiement arrivent sous la forme de `PaymentEvent.CreationFailed(cause)`, où `cause` est une
`AcceptException` typée. Voici celles propres à cette intégration :

| Exception                                          | Quand elle se produit                                                                 | Que faire                                                                               |
| :------------------------------------------------- | :------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------- |
| `PluginUnavailable`                                | Le plugin Tapaya Terminal n’est pas installé ou la liaison avec son service a échoué. | Invitez l’utilisateur à l’installer avec `Accept.plugin.install()`.                     |
| `CurrencyNotAvailableForMerchant(code)`            | Le commerçant n’est pas configuré pour la devise demandée.                            | Débitez dans une devise activée, ou mettez à jour la configuration du commerçant.       |
| `AmountBelowMinimum(minimum, currency)`            | Le montant est inférieur au minimum configuré.                                        | Débitez un montant égal ou supérieur à `minimum`.                                       |
| `TransactionTimeoutOutOfRange(transactionTimeout)` | `transactionTimeout` était hors de la plage de 0 à 120 secondes.                      | Transmettez une valeur dans la plage, ou omettez-la pour utiliser la valeur par défaut. |
| `LocationPermissionRequired`                       | Votre application ne dispose pas de l’autorisation de localisation.                   | Demandez `ACCESS_FINE_LOCATION`, puis réessayez.                                        |
| `LocationUnavailable` / `LocationTimeout`          | La position de l’appareil n’a pas pu être déterminée.                                 | Demandez à l’utilisateur d’activer la localisation, puis réessayez.                     |
| `SessionExpired`                                   | La session authentifiée a expiré.                                                     | Authentifiez-vous de nouveau avec `Accept.auth.authenticate()`.                         |
| `NoInternetConnection`                             | Aucune connexion réseau.                                                              | Demandez à l’utilisateur de se reconnecter, puis réessayez.                             |
| `PaymentInProgress`                                | Un appel précédent à `pay()` attend toujours un résultat.                             | Attendez la fin du paiement en cours.                                                   |

`Accept.payments.status()`, `cancel()` et `refund()` sont des fonctions `suspend` et **lèvent** ces exceptions
(au lieu d’émettre des événements) ; encadrez-les d’un `try-catch`. `status()`/`cancel()`/`refund()` lèvent aussi
`PaymentNotFound` pour un jeton inconnu. Consultez la liste complète dans
[Gestion des erreurs](../integration/sdk_integration#error-handling).

## Délais d’expiration [#timeouts]

Un paiement par carte doit être finalisé sur le terminal peu après son démarrage. Si le client ne présente jamais sa carte, ou si le
plugin Tapaya Terminal est fermé en cours de paiement, le paiement se termine en `PayResult.Canceled` ou
`PayResult.Failed`, et le paiement sous-jacent est annulé côté serveur et n’est jamais finalisé. Confirmez toujours
l’état final avec `Accept.payments.status()` avant de considérer un paiement comme terminé.

## Intégrer sans le SDK [#integrate-without-the-sdk]

Si vous ne pouvez pas intégrer l’Accept SDK (framework multiplateforme, coque POS légère, caisse web), vous pouvez piloter
Tapaya Terminal directement avec un deeplink. La répartition des tâches change : c’est **vous** qui créez le paiement via
l’API Tapaya et **vous** qui interrogez son statut pour obtenir le résultat ; Tapaya Terminal ne gère que l’interaction avec la carte.

Le déroulement :

1. Créez un paiement depuis votre backend via la [Payments API](https://api.tapaya.com/openapi/index.html) et conservez
   son `paymentId`.
2. Ouvrez Tapaya Terminal avec ce `paymentId` à l’aide du [deeplink de paiement](#pay-deeplink).
3. Interrogez régulièrement le statut du paiement via la [Payments API](https://api.tapaya.com/openapi/index.html) jusqu’à ce qu’il quitte
   `PENDING`. Les deeplinks n’ont pas de canal de retour ; l’interrogation est donc la seule source de vérité.

<Callout title="Remarque" type="info">
  Tapaya Terminal doit être installé, connecté au compte du commerçant et configuré pour la devise
  du paiement. Ce mode ne comporte aucune étape d’activation ; le compte connecté est le commerçant officiel (merchant of record). Si nécessaire,
  connectez un commerçant avec le [deeplink de connexion](#login-deeplink).
</Callout>

### Schémas de deeplink [#deeplink-schemes]

Le schéma détermine quelle version de Tapaya Terminal traite le lien :

| Environnement | Schéma                  |
| :------------ | :---------------------- |
| Production    | `tapaya-accept`         |
| Sandbox       | `tapaya-accept-sandbox` |

Si un lien ne s’ouvre pas, cette version de Tapaya Terminal n’est pas installée.

### Deeplink de paiement [#pay-deeplink]

```text
tapaya-accept://pay?paymentId=<paymentId>&amount=<amount>&currency=<ISO4217>
```

| Paramètre de requête | Obligatoire | Remarques                                                                                                                                                 |
| :------------------- | :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentId`          | **Oui**     | Le paiement créé à l’étape 1.                                                                                                                             |
| `amount`             | **Oui**     | Montant en **unités principales**, sous forme de nombre entier : `amount=15` débite 15,00. Notez que cela diffère du SDK, qui attend des unités mineures. |
| `currency`           | **Oui**     | Code de devise ISO 4217, par exemple `CZK`.                                                                                                               |

<CodeBlockTabs defaultValue="kotlin" groupId="accept-language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="kotlin">
      Android (Kotlin)
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="typescript">
      React Native (TypeScript)
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="kotlin">
    ```kotlin
    val uri = Uri.parse("tapaya-accept://pay?paymentId=$paymentId&amount=15&currency=CZK")
    try {
        startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
    } catch (e: ActivityNotFoundException) {
        // Tapaya Terminal is not installed, send the user to Google Play
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="typescript">
    ```typescript
    import { Linking } from 'react-native';

    const url = `tapaya-accept://pay?paymentId=${paymentId}&amount=15&currency=CZK`;
    try {
        await Linking.openURL(url);
    } catch {
        // Tapaya Terminal is not installed, send the user to Google Play
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

### Deeplink de connexion [#login-deeplink]

Connectez un commerçant à Tapaya Terminal avec son `merchantToken`, l’identifiant de commerçant que vous avez défini avec `POST /merchant/auth/register` :

```text
tapaya-accept://login?token=<merchantToken>
```

| Paramètre de requête | Obligatoire | Remarques                                                                                                                                                 |
| :------------------- | :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`              | **Oui**     | Le `merchantToken` du commerçant. Il s’agit de votre ID de commerçant stable, et non du jeton SDK de courte durée obtenu via `POST /merchant/auth/login`. |
| `activationCode`     | Non         | Préremplit le code d’activation du terminal.                                                                                                              |

## Tester l’intégration [#test-the-integration]

1. Initialisez avec `isProduction = false` (valeur par défaut) ; les paiements en sandbox ne déplacent jamais d’argent réel.
2. Installez le plugin Tapaya Terminal sur votre appareil de test, connectez-vous avec un commerçant de la sandbox, puis activez-le.
3. Effectuez un paiement d’un petit montant et vérifiez le parcours `Success` de bout en bout, y compris votre interface de reçu.
4. Testez les cas d’échec : refusez un paiement sur le terminal, annulez-en un et démarrez-en un dans une devise
   non prise en charge ; votre gestion de `PayResult`/`CreationFailed` doit couvrir chacun d’eux.

Lorsque vous êtes prêt à passer en production, définissez `isProduction = true` dans `initialize` et authentifiez-vous avec des identifiants
de production. Le code de paiement ne change pas.
