Tapaya
Acceptation des paiements en magasinGuide d’intégrationApplication Tapaya Terminal

Application Tapaya Terminal

Acceptez les paiements par carte sans contact en confiant l’interaction avec la carte à l’application plugin Tapaya 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

OptionIdéal pourRemise du résultat
Accept SDK (recommandé)Applications Android nativesAccept.payments.pay() émet un Flow<PaymentEvent> : création, lancement et résultat final du terminal, gérés pour vous
DeeplinkWebViews, frameworks multiplateformes, QR codesVous 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 pour le contrat du deeplink.

Fonctionnement

  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

  • Le plugin Tapaya Terminal installé sur l’appareil et activé pour votre commerçant (voir Installer et activer le terminal).
  • Un appareil équipé du NFC, sous Android 11 (API 30) ou version ultérieure.
  • L’Accept SDK installé et initialisé, avec le commerçant connecté.
  • L’autorisation de localisation (ACCESS_FINE_LOCATION) accordée à votre application à l’exécution.

Installer et activer le 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.

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
}

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

Remarque

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.

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

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.

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 */ }
}

Remarque

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.

Créer un paiement

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.

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
        }
    }

Paramètres :

ParamètreTypeObligatoireDescription
amountLong✓Montant à débiter, dans l’unité mineure de currency (par exemple, les cents pour USD, les hellers pour CZK).
currencyString✓Code de devise ISO 4217.
paymentTokenString?Reprend une session de paiement existante au lieu d’en démarrer une nouvelle. Vaut null par défaut.
receiptConfigPaymentReceiptConfig?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.
nfcPositionNfcPositionConfig?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.
metadataMap<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.
tipLong?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).
transactionTimeoutDuration?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.
displayPaymentDisplay?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.

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

Le renvoi de metadata n’est pas garanti

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.

Choisir la devise

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 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 pour la liste complète des exceptions.

Configurer l’écran de reçu

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.

PaymentReceiptConfigDescription
DismissImmediatelyRien 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.

// 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,
)

Positionner l’animation de paiement sans contact NFC

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.

NfcPositionConfigDescription
UnspecifiedAucun 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.
ExternalAboveUn 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

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.

Accept.payments.pay(
    amount = 1500,
    currency = "CZK",
    transactionTimeout = 90.seconds,
)

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

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.

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,
)
PaymentDisplayDescription
DefaultL’écran actuel de l’application hôte (comportement par défaut, inchangé sur les appareils à écran unique).
CustomerFacingLe 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 = ...).

Définissez la cible avant l’authentification

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.

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

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

PayResultSignification
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

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.

ChampTypeDescription
merchantIdString?ID du commerçant transmis par le processeur.
terminalIdString?ID du terminal transmis par le processeur.
cardBrandString?Marque de la carte, par exemple Visa ou Mastercard.
appLabelString?Libellé de l’application EMV.
aidString?Identifiant de l’application EMV.
maskedPanString?Numéro de carte masqué.
responseCodeString?Code de réponse du processeur.
transactionTimestampString?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

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() :

val status: PaymentStatus = Accept.payments.status(paymentToken)
status.state       // PENDING, SUCCESS, CANCELLED, REFUNDED, FAILED, ACTION_NEEDED
status.receiptUrl  // for your receipt UI

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

Effectuer un remboursement

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

val state: PaymentState = Accept.payments.refund(paymentToken, RefundReason.CUSTOMER_REQUEST)

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

Gestion des erreurs

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 :

ExceptionQuand elle se produitQue faire
PluginUnavailableLe 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.
LocationPermissionRequiredVotre application ne dispose pas de l’autorisation de localisation.Demandez ACCESS_FINE_LOCATION, puis réessayez.
LocationUnavailable / LocationTimeoutLa position de l’appareil n’a pas pu être déterminée.Demandez à l’utilisateur d’activer la localisation, puis réessayez.
SessionExpiredLa session authentifiée a expiré.Authentifiez-vous de nouveau avec Accept.auth.authenticate().
NoInternetConnectionAucune connexion réseau.Demandez à l’utilisateur de se reconnecter, puis réessayez.
PaymentInProgressUn 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.

Délais d’expiration

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

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 et conservez son paymentId.
  2. Ouvrez Tapaya Terminal avec ce paymentId à l’aide du deeplink de paiement.
  3. Interrogez régulièrement le statut du paiement via la Payments API 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é.

Remarque

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.

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

EnvironnementSchéma
Productiontapaya-accept
Sandboxtapaya-accept-sandbox

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

tapaya-accept://pay?paymentId=<paymentId>&amount=<amount>&currency=<ISO4217>
Paramètre de requêteObligatoireRemarques
paymentIdOuiLe paiement créé à l’étape 1.
amountOuiMontant 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.
currencyOuiCode de devise ISO 4217, par exemple CZK.
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
}

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

tapaya-accept://login?token=<merchantToken>
Paramètre de requêteObligatoireRemarques
tokenOuiLe 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.
activationCodeNonPréremplit le code d’activation du terminal.

Tester l’intégration

  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.

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