# Tapaya Terminal App (/de/in-store/integration/card_terminal)



Nehmen Sie kontaktlose Kartenzahlungen in Ihrer Android-App an, indem Sie die Interaktion mit der Karte an die Plugin-App **Tapaya Terminal**
übergeben. Ihre App behält die Kontrolle über den Verkauf; das Plugin übernimmt das Vorhalten der Karte per NFC, die PIN-Eingabe und die
Kartenverarbeitung, und das Ergebnis fließt anschließend über den Event-Stream von `Accept.payments.pay()` zurück.

Verwenden Sie diese Integration, wenn Sie Tap to Pay anbieten möchten, ohne einen Zahlungsabwickler in Ihre eigene App einzubetten. Ist für Ihre
Plattform stattdessen eingebettetes Tap to Pay freigeschaltet, lesen Sie den Leitfaden *Embedded Tap to Pay*.

## Integrationsoptionen [#integration-options]

| Option                     | Geeignet für                                          | Übermittlung des Ergebnisses                                                                                                                       |
| :------------------------- | :---------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Accept SDK** (empfohlen) | Native Android-Apps                                   | `Accept.payments.pay()` gibt einen `Flow<PaymentEvent>` aus: Erstellung, Start und das endgültige Ergebnis vom Terminal werden für Sie abgewickelt |
| **Deeplink**               | WebViews, plattformübergreifende Frameworks, QR-Codes | Sie erstellen die Zahlung über die Tapaya API und fragen ihren Status regelmäßig ab; das Plugin öffnet sich über einen Deeplink                    |

Zuerst wird der Weg über das SDK beschrieben; die Spezifikation des Deeplinks finden Sie unter [Integration ohne SDK](#integrate-without-the-sdk).

## So funktioniert es [#how-it-works]

1. Ihre App startet eine Zahlung mit `Accept.payments.pay(amount, currency)`.
2. Das SDK registriert die Zahlung beim Tapaya-Backend und startet das Tapaya-Terminal-Plugin auf demselben Gerät.
3. Der Kunde hält seine Karte oder sein Telefon an den Bildschirm von Tapaya Terminal.
4. Tapaya Terminal schließt die Zahlung mit dem Tapaya-Backend ab.
5. Das SDK beobachtet die Zahlung, bis sie einen Endzustand erreicht, und gibt `PaymentEvent.Result` mit einem
   `PayResult` aus. Kann die Zahlung überhaupt nicht erstellt werden, gibt es stattdessen `PaymentEvent.CreationFailed` aus, in der Regel
   innerhalb einer Sekunde.

## Voraussetzungen [#prerequisites]

* Das **Tapaya-Terminal-Plugin** ist auf dem Gerät installiert und für Ihren Händler **aktiviert** (siehe
  [Terminal installieren und aktivieren](#install-and-activate-the-terminal)).
* Ein Gerät mit NFC und Android 11 (API 30) oder neuer.
* Das Accept SDK ist [installiert und initialisiert](../integration/sdk_integration#initialization), und der Händler ist
  [angemeldet](../integration/sdk_integration#step-2-authenticate).
* Die Standortberechtigung (`ACCESS_FINE_LOCATION`) wurde Ihrer App zur Laufzeit erteilt.

## Terminal installieren und aktivieren [#install-and-activate-the-terminal]

Die Aktivierung erfolgt einmal pro Gerät. Sie verknüpft das Tapaya-Terminal-Plugin mit demselben Händlerkonto, bei dem Ihre App
angemeldet ist.

<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` ist einer der Werte `INVALID_INTENT`, `HANDOVER_FAILED`, `CONFIRM_FAILED`,
`MERCHANT_NOT_ONBOARDED` oder `UNKNOWN`.

<Callout title="Hinweis" type="info">
  `Accept.plugin.isInstalled()` gibt `false` zurück, wenn das Tapaya-Terminal-Plugin fehlt. Den aktuellen Zustand des Plugins
  können Sie jederzeit mit `Accept.plugin.status()` abfragen; sichern Sie Ihre Zahlungsoberfläche entsprechend ab.
</Callout>

`activateTerminal()` **wirft** außerdem `NoTidsAvailableForMerchant`, wenn dem Händler kein nicht zugewiesenes Terminal
(TID) mehr zur Aktivierung bleibt, und zwar getrennt von den oben genannten Ergebnissen `ActivateTerminalResult.Failed`. Umschließen Sie den Aufruf mit
`try-catch`, wenn Sie diesen Fall gesondert anzeigen möchten, zum Beispiel indem Sie den Händler auffordern, eine TID freizugeben oder
eine weitere anzufordern.

### Nach Plugin-Updates suchen [#checking-for-plugin-updates]

`Accept.plugin.updateInfo()` meldet, ob auf Google Play ein neuerer Build des Tapaya-Terminal-Plugins verfügbar ist,
sodass Sie den Händler zum Aktualisieren auffordern können, bevor eine Zahlung auf eine Versionslücke trifft.

<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="Hinweis" type="info">
  Das SDK fragt Google Play nie direkt ab; die Plugin-App prüft dies selbst über Play Core und meldet das Ergebnis über
  ihren bestehenden Statuskanal. Ein Plugin-Build, der älter ist als der, mit dem diese Funktion ausgeliefert wurde, meldet immer
  `Unknown`.
</Callout>

## Zahlung erstellen [#create-a-payment]

Beträge werden immer in der **kleinsten Einheit der Währung** angegeben: Heller für CZK, Cent für EUR. `1500` mit
der Währung `"CZK"` belastet 15,00 Kč. Währungen sind ISO-4217-Codes.

`pay()` gibt einen `Flow<PaymentEvent>` zurück; sammeln Sie ihn, um den Fortschritt und das endgültige Ergebnis zu verfolgen. Die Methode wirft nie eine Exception;
Fehler zeigen sich als `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>

**Parameter:**

| Parameter            | Typ                     | Erforderlich | Beschreibung                                                                                                                                                                                                                                                              |
| :------------------- | :---------------------- | :----------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `amount`             | `Long`                  | ✓            | Zu belastender Betrag in der kleinsten Einheit von `currency` (z. B. Cent für USD, Heller für CZK).                                                                                                                                                                       |
| `currency`           | `String`                | ✓            | ISO-4217-Währungscode.                                                                                                                                                                                                                                                    |
| `paymentToken`       | `String?`               |              | Setzt eine bestehende Zahlungs-Session fort, statt eine neue zu starten. Standardwert ist `null`.                                                                                                                                                                         |
| `receiptConfig`      | `PaymentReceiptConfig?` |              | Überschreibt den Belegbildschirm, der nach einer **genehmigten** Zahlung angezeigt wird, nur für diesen Aufruf. Standardwert ist `null`; dann gilt der mit `Accept.payments.setOptions()` festgelegte Standard.                                                           |
| `nfcPosition`        | `NfcPositionConfig?`    |              | Überschreibt, worauf die Tap-to-Pay-Animation zeigt, nur für diesen Aufruf. Standardwert ist `null`; dann gilt der mit `Accept.payments.setOptions()` festgelegte Standard.                                                                                               |
| `metadata`           | `Map<String, String>`   |              | Schlüssel-Wert-Daten, die an die Zahlung angehängt werden. Werden im Intent `PAY` an das Tapaya-Terminal-Plugin gesendet und im resultierenden `PayResult` zurückgegeben, sofern das Plugin sie zurückliefert. Standardwert ist `emptyMap()`.                             |
| `tip`                | `Long?`                 |              | Festes Trinkgeld in der kleinsten Einheit von `currency`. Ist es gesetzt, überspringt das Plugin seinen eigenen interaktiven Trinkgeld-Bildschirm und belastet `amount + tip`. Standardwert ist `null` (das Plugin fragt den Zahlenden nach einem Trinkgeld).             |
| `transactionTimeout` | `Duration?`             |              | Überschreibt, wie lange das Terminal auf das Vorhalten einer Karte wartet, nur für diesen Aufruf. Standardwert ist `null`; dann gilt der mit `Accept.payments.setOptions()` festgelegte Standard. Siehe [Transaktions-Timeout festlegen](#setting-a-transaction-timeout). |
| `display`            | `PaymentDisplay?`       |              | Überschreibt, auf welchem Bildschirm die Zahlungsoberfläche des Plugins läuft, nur für diesen Aufruf. Standardwert ist `null`; dann gilt der mit `Accept.payments.setOptions()` festgelegte Standard. Siehe [Display auswählen](#targeting-a-display).                    |

Während die Zahlung läuft, ist das Tapaya-Terminal-Plugin im Vordergrund. Ihre App wird automatisch fortgesetzt,
sobald die Zahlung abgeschlossen ist. Um eine unterbrochene Zahlung fortzusetzen, übergeben Sie ihr `paymentToken` erneut an
`pay(amount, currency, paymentToken)`.

<Callout title="Rückgabe von metadata ohne Gewähr" type="info">
  `metadata` wird in `PayResult` nur zurückgegeben, wenn das Plugin es zurückliefert. Behandeln Sie es daher als Kontext ohne Gewähr
  (z. B. eine Bestell-ID für Ihr eigenes Logging), nicht als Daten, auf deren Rückgabe Ihre App angewiesen ist.
</Callout>

### Währung wählen [#choosing-the-currency]

`pay()` belastet den Betrag in der übergebenen `currency`, angegeben als ISO-4217-Code in Großbuchstaben, mit `amount` in der kleinsten
Einheit dieser Währung. Unter [Unterstützte Währungen](/merchants/supported_currencies) finden Sie alle Währungen, die Tapaya unterstützt, ein Händler
ist jedoch meist nur für einige davon freigeschaltet.

Rufen Sie `Accept.merchant.availableCurrencies()` auf, um die ISO-4217-Codes zu erhalten, für die der Händler freigeschaltet ist. Eine Zahlung in jeder anderen
Währung schlägt mit `CurrencyNotAvailableForMerchant` fehl.

Jede Währung hat außerdem einen Mindestbetrag, der plattformweit durchgesetzt wird. Die aktuellen Mindestbeträge rufen Sie mit
`Accept.sdk.minimumAmounts()` ab, das eine `List<MinimumAmount>` zurückgibt (`currency: String, amount: Long`, in der kleinsten
Einheit der Währung). Ein Aufruf von `pay()` unterhalb dieses Mindestbetrags schlägt mit
`PaymentEvent.CreationFailed(AmountBelowMinimum(minimum, currency))` fehl. Die vollständige Liste der Exceptions finden Sie unter
[Fehlerbehandlung](./sdk_integration#error-handling).

### Belegbildschirm konfigurieren [#configuring-the-receipt-screen]

`receiptConfig` legt fest, was das Tapaya-Terminal-Plugin dem Zahlenden nach einer **genehmigten** Zahlung anzeigt, wie
lange es auf dem Bildschirm bleibt und ob gedruckt wird. Auf abgelehnte, stornierte oder auf Stornierung wartende Ergebnisse
hat es keinen Einfluss; diese zeigen unabhängig von dieser Einstellung immer ihren eigenen Ergebnisbildschirm.

| `PaymentReceiptConfig`                                    | Beschreibung                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DismissImmediately`                                      | Dem Zahlenden wird nichts angezeigt.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `Show(showQrCodeReceipt, dismissBehavior, printBehavior)` | Zeigt den Belegbildschirm. `showQrCodeReceipt: Boolean = true` schaltet einen QR-Code mit Link zum Beleg ein oder aus. `dismissBehavior: DismissBehavior = DismissBehavior.Manual` legt fest, wie er geschlossen wird: `Manual` (bleibt, bis der Zahlende ihn schließt) oder `Delayed(delay: Duration)` (schließt sich nach `delay` automatisch, sofern der Zahlende ihn nicht vorher geschlossen hat). `printBehavior: ReceiptPrintBehavior = ReceiptPrintBehavior.OnDemand` steuert den gedruckten Beleg: `Automatic` (wird gedruckt, sobald der Bildschirm erscheint), `OnDemand` (nur wenn der Kassierer den Druck auslöst) oder `Disabled` (wird nie gedruckt). |

Legen Sie einmalig mit `Accept.payments.setOptions()` einen geräteweiten Standard fest und überschreiben Sie ihn für eine einzelne Zahlung über
`pay(..., receiptConfig = ...)`. Solange `setOptions()` nicht aufgerufen wurde, ist der Standard `Show` mit QR-Beleg, manuellem
Schließen und Druck auf Anforderung, was dem eigenen Fallback des Plugins entspricht.

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

### NFC-Animation positionieren [#positioning-the-nfc-tap-animation]

`nfcPosition` ist ein Hinweis, der dem Tapaya-Terminal-Plugin mitteilt, wo sich die NFC-Antenne dieses Geräts befindet, damit es die
Animation „Hier vorhalten“ richtig platzieren kann. Das Plugin ermittelt die Position der Antenne zunächst selbst (Live-Erkennung über die Plattform,
dann eine mitgelieferte Gerätedatenbank, dann alles, was der Händler direkt im Plugin konfiguriert hat); geben Sie diesen Wert nur an,
um diese Ermittlung zu überschreiben, zum Beispiel bei einem Gerät, das das Plugin nicht erkennen kann oder das in seiner Datenbank falsch hinterlegt ist.

| `NfcPositionConfig`                       | Beschreibung                                                                                                                                                                    |
| :---------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Unspecified`                             | Keine Überschreibung (Standard). Das Plugin ermittelt die Position selbst und behält eine Position bei, die der Händler dort bereits konfiguriert hat.                          |
| `OnDevice(surface, xFraction, yFraction)` | Die Antenne befindet sich im Gerät selbst; die Animation zeigt auf `(xFraction, yFraction)` auf `surface`, entweder `OnDeviceSurface.BACK` oder `OnDeviceSurface.FRONT_SCREEN`. |
| `ExternalAbove`                           | Ein separates Lesermodul sitzt über dem Telefon (Smart-POS-Halterung); auf dem Gerät gibt es keinen Punkt, auf den die Animation zeigen könnte.                                 |

Legen Sie mit `Accept.payments.setOptions(nfcPosition = ...)` einen geräteweiten Standard fest oder überschreiben Sie ihn pro Aufruf über
`pay(..., nfcPosition = ...)`. `setOptions()` ändert nur die übergebenen Optionen, daher stört `setOptions(nfcPosition = ...)`
allein keinen zuvor gesetzten Standard für `receiptConfig` und umgekehrt.

### Transaktions-Timeout festlegen [#setting-a-transaction-timeout]

`transactionTimeout` legt fest, wie lange das Tapaya-Terminal-Plugin darauf wartet, dass der Kunde seine Karte vorhält, bevor
es aufgibt. Es akzeptiert jede `Duration` von null bis 120 Sekunden; alles außerhalb dieses Bereichs zeigt sich als
`TransactionTimeoutOutOfRange(transactionTimeout)` über `PaymentEvent.CreationFailed`, da `pay()` nie synchron
eine Exception wirft.

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

Belassen Sie den Standardwert `null`, um den eigenen Fallback der Begleit-App von etwa 60 Sekunden zu verwenden, oder legen Sie mit
`Accept.payments.setOptions(transactionTimeout = ...)` einen geräteweiten Standard fest.

### Display auswählen [#targeting-a-display]

Bei einem zweiseitigen Gerät („double-sided“) ist ein Panel dem Händler zugewandt und ein zweites, kundenseitiges Panel dem
Zahlenden. `Accept.displays` meldet, welche Displays das Gerät gerade hat, und die Option `display` teilt dem Plugin mit,
auf welchem Panel es seine Zahlungsoberfläche ausführen soll.

<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`      | Beschreibung                                                                                             |
| :-------------------- | :------------------------------------------------------------------------------------------------------- |
| `Default`             | Das aktuelle Display der Host-App (Standardverhalten, auf Geräten mit nur einem Bildschirm unverändert). |
| `CustomerFacing`      | Das sekundäre Panel eines zweiseitigen Geräts.                                                           |
| `Specific(displayId)` | Ein explizit angegebenes Display, über die in `Accept.displays.all()` gemeldete `id`.                    |

Ein Ziel, das nicht umgesetzt werden kann, zum Beispiel `CustomerFacing` auf einem Gerät mit nur einem Bildschirm, fällt stillschweigend auf
`Default` zurück. Legen Sie mit `Accept.payments.setOptions(display = ...)` einen geräteweiten Standard fest oder überschreiben Sie ihn pro Aufruf über
`pay(..., display = ...)`.

<Callout title="Ziel vor der Authentifizierung festlegen" type="info">
  Das Aufwärmen des Terminals (Warm-up), das `Accept.auth.authenticate()` auslöst, folgt demselben Ziel. Rufen Sie daher
  `setOptions(display = ...)` vor `authenticate()` auf, wenn auch das Aufwärmen selbst nicht auf dem Bildschirm des Händlers
  stattfinden soll.
</Callout>

Die Beispiel-App `:dualscreen` zeigt einen vollständigen Bezahlvorgang auf zwei Panels: Der Händler arbeitet auf dem primären Panel, während
`PaymentDisplay.CustomerFacing` die Zahlungsoberfläche des Plugins auf den Bildschirm des Kunden bringt.

## Zahlungsergebnis [#payment-outcome]

Das abschließende `PaymentEvent.Result` enthält ein `PayResult`. Jede Variante enthält `metadata` und gibt damit zurück, was Sie
an `pay()` übergeben haben, sofern das Plugin es zurückliefert (kann `null` sein):

| `PayResult`                                                 | Bedeutung                                                                                                                                                                                                                                   |
| :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Success(paymentToken, metadata, authCode, receiptDetails)` | Autorisiert und erfasst. Die Gelder werden zugunsten des Händlers abgerechnet. `authCode` ist der vom Terminal gemeldete Autorisierungscode des Zahlungsabwicklers; er dient nur zur Information und kann je nach Zahlungsabwickler fehlen. |
| `Declined(metadata, receiptDetails)`                        | Vom Zahlungsabwickler oder von der kartenausgebenden Bank abgelehnt.                                                                                                                                                                        |
| `Canceled(metadata)`                                        | Vom Kunden oder Händler vor dem Abschluss abgebrochen.                                                                                                                                                                                      |
| `Failed(reason, metadata)`                                  | Nicht abgeschlossen; `reason` ist ein `PayFailureReason`.                                                                                                                                                                                   |

`PayFailureReason` ist einer der Werte `INVALID_INTENT`, `NOT_ACTIVATED`, `TERMINAL_INIT_FAILED`, `PAYMENT_SETUP_FAILED`,
`PLUGIN_UNAVAILABLE` oder `UNKNOWN`.

### Belegdaten auslesen [#reading-receipt-details]

`Success` und `Declined` enthalten optional `receiptDetails: ReceiptDetails?` mit strukturierten, vom Terminal gemeldeten Daten
für den Kartenbeleg. Sie sind für Aufrufer gedacht, die ihren eigenen offiziellen Kartenbeleg drucken, statt sich allein auf
die gehostete Seite unter `PaymentStatus.receiptUrl` zu verlassen.

| Feld                   | Typ       | Beschreibung                                    |
| :--------------------- | :-------- | :---------------------------------------------- |
| `merchantId`           | `String?` | Vom Zahlungsabwickler gemeldete Händler-ID.     |
| `terminalId`           | `String?` | Vom Zahlungsabwickler gemeldete Terminal-ID.    |
| `cardBrand`            | `String?` | Kartenmarke, z. B. Visa oder Mastercard.        |
| `appLabel`             | `String?` | EMV-Anwendungsbezeichnung.                      |
| `aid`                  | `String?` | EMV-Anwendungskennung.                          |
| `maskedPan`            | `String?` | Maskierte Kartennummer.                         |
| `responseCode`         | `String?` | Antwortcode des Zahlungsabwicklers.             |
| `transactionTimestamp` | `String?` | Zeitstempel der Transaktion im Format ISO-8601. |

Jedes Feld ist optional und ohne Gewähr; ein Aufrufer muss damit rechnen, dass jedes davon, oder `receiptDetails` selbst,
fehlt.

### Zahlungsdetails auslesen [#reading-payment-details]

`PayResult.Success` liefert Ihnen das `paymentToken`. Den vollständigen Datensatz, einschließlich `receiptUrl`, angeforderter und
abgerechneter Beträge sowie Zeitstempeln, rufen Sie mit `Accept.payments.status()` ab:

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

Eine laufende Zahlung stoppen Sie mit `Accept.payments.cancel(paymentToken)`, das den resultierenden `PaymentState` zurückgibt.

### Erstattung ausführen [#issuing-a-refund]

Eine abgeschlossene Zahlung erstatten Sie mit `Accept.payments.refund(paymentToken, reason)`, das ebenfalls den resultierenden
`PaymentState` zurückgibt.

<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` ist einer der Werte `CUSTOMER_REQUEST`, `DUPLICATE`, `FRAUDULENT` oder `EXPIRED_CHARGE`.

## Fehlerbehandlung [#error-handling]

Eine Zahlung, die das Terminal erreicht, aber nicht erfolgreich ist, ist **keine** Exception; sie kommt als
`PaymentEvent.Result` mit `PayResult.Declined`, `Canceled` oder `Failed(reason)` an. Fehler, die das Erstellen der
Zahlung verhindern, kommen als `PaymentEvent.CreationFailed(cause)` an, wobei `cause` eine typisierte
`AcceptException` ist. Die für diese Integration spezifischen Exceptions:

| Exception                                          | Wann sie auftritt                                                                                | Was zu tun ist                                                                                                   |
| :------------------------------------------------- | :----------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------- |
| `PluginUnavailable`                                | Das Tapaya-Terminal-Plugin ist nicht installiert, oder sein Dienst konnte nicht gebunden werden. | Fordern Sie mit `Accept.plugin.install()` zur Installation auf.                                                  |
| `CurrencyNotAvailableForMerchant(code)`            | Der Händler ist für die angeforderte Währung nicht konfiguriert.                                 | Belasten Sie den Betrag in einer freigeschalteten Währung oder aktualisieren Sie die Konfiguration des Händlers. |
| `AmountBelowMinimum(minimum, currency)`            | Der Betrag liegt unter dem konfigurierten Mindestbetrag.                                         | Belasten Sie mindestens `minimum`.                                                                               |
| `TransactionTimeoutOutOfRange(transactionTimeout)` | `transactionTimeout` lag außerhalb des Bereichs von 0 bis 120 Sekunden.                          | Übergeben Sie einen Wert innerhalb des Bereichs oder lassen Sie ihn weg, um den Standard zu verwenden.           |
| `LocationPermissionRequired`                       | Ihrer App wurde keine Standortberechtigung erteilt.                                              | Fordern Sie `ACCESS_FINE_LOCATION` an und versuchen Sie es erneut.                                               |
| `LocationUnavailable` / `LocationTimeout`          | Der Gerätestandort konnte nicht ermittelt werden.                                                | Bitten Sie den Nutzer, den Standort zu aktivieren, und versuchen Sie es erneut.                                  |
| `SessionExpired`                                   | Die authentifizierte Session ist abgelaufen.                                                     | Authentifizieren Sie sich erneut mit `Accept.auth.authenticate()`.                                               |
| `NoInternetConnection`                             | Keine Netzwerkverbindung.                                                                        | Bitten Sie den Nutzer, die Verbindung wiederherzustellen, und versuchen Sie es erneut.                           |
| `PaymentInProgress`                                | Ein vorheriger Aufruf von `pay()` wartet noch auf ein Ergebnis.                                  | Warten Sie, bis die aktuelle Zahlung abgeschlossen ist.                                                          |

`Accept.payments.status()`, `cancel()` und `refund()` sind `suspend`-Funktionen und **werfen** diese Exceptions
(statt Events auszugeben); umschließen Sie sie mit `try-catch`. `status()`/`cancel()`/`refund()` werfen außerdem
`PaymentNotFound` für ein unbekanntes Token. Die vollständige Liste finden Sie unter
[Fehlerbehandlung](../integration/sdk_integration#error-handling).

## Timeouts [#timeouts]

Eine Kartenzahlung muss kurz nach ihrem Start am Terminal abgeschlossen werden. Hält der Kunde die Karte nie vor oder wird das
Tapaya-Terminal-Plugin während der Zahlung geschlossen, endet die Zahlung als `PayResult.Canceled` oder
`PayResult.Failed`, und die zugrunde liegende Zahlung wird serverseitig abgebrochen und nie abgeschlossen. Bestätigen Sie immer den
Endzustand mit `Accept.payments.status()`, bevor Sie eine Zahlung als abgeschlossen behandeln.

## Integration ohne SDK [#integrate-without-the-sdk]

Wenn Sie das Accept SDK nicht einbetten können (ein plattformübergreifendes Framework, eine schlanke POS-Shell, eine webbasierte Kasse), können Sie
Tapaya Terminal direkt über einen Deeplink steuern. Dabei ändert sich die Aufgabenteilung: **Sie** erstellen die Zahlung über die
Tapaya API, und **Sie** fragen ihren Status regelmäßig ab, um das Ergebnis zu erhalten; Tapaya Terminal übernimmt nur die Interaktion mit der Karte.

Der Ablauf:

1. Erstellen Sie über Ihr Backend eine Zahlung mit der [Payments API](https://api.tapaya.com/openapi/index.html) und speichern Sie
   ihre `paymentId`.
2. Öffnen Sie Tapaya Terminal mit dieser `paymentId` über den [Zahlungs-Deeplink](#pay-deeplink).
3. Fragen Sie den Zahlungsstatus regelmäßig über die [Payments API](https://api.tapaya.com/openapi/index.html) ab, bis er
   `PENDING` verlässt. Deeplinks haben keinen Rückkanal, daher ist die Abfrage die einzige verlässliche Quelle für das Ergebnis.

<Callout title="Hinweis" type="info">
  Tapaya Terminal muss installiert, beim Händlerkonto angemeldet und für die Währung der Zahlung
  konfiguriert sein. In diesem Modus gibt es keinen Aktivierungsschritt; das angemeldete Konto ist der Merchant of Record (für die Transaktion verantwortlicher Händler). Melden Sie
  einen Händler bei Bedarf mit dem [Login-Deeplink](#login-deeplink) an.
</Callout>

### Deeplink-Schemata [#deeplink-schemes]

Das Schema bestimmt, welcher Build von Tapaya Terminal den Link verarbeitet:

| Umgebung   | Schema                  |
| :--------- | :---------------------- |
| Produktion | `tapaya-accept`         |
| Sandbox    | `tapaya-accept-sandbox` |

Lässt sich ein Link nicht auflösen, ist dieser Build von Tapaya Terminal nicht installiert.

### Zahlungs-Deeplink [#pay-deeplink]

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

| Query-Parameter | Erforderlich | Hinweise                                                                                                                                            |
| :-------------- | :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentId`     | **Ja**       | Die in Schritt 1 erstellte Zahlung.                                                                                                                 |
| `amount`        | **Ja**       | Betrag in **Haupteinheiten** als ganze Zahl: `amount=15` belastet 15,00. Beachten Sie, dass dies vom SDK abweicht, das kleinste Einheiten erwartet. |
| `currency`      | **Ja**       | ISO-4217-Währungscode, z. B. `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>

### Login-Deeplink [#login-deeplink]

Melden Sie einen Händler mit seinem `merchantToken` bei Tapaya Terminal an. Das ist die Händlerkennung, die Sie mit `POST /merchant/auth/register` festlegen:

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

| Query-Parameter  | Erforderlich | Hinweise                                                                                                                            |
| :--------------- | :----------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| `token`          | **Ja**       | Das `merchantToken` des Händlers. Dies ist Ihre stabile Händler-ID, nicht das kurzlebige SDK-Token aus `POST /merchant/auth/login`. |
| `activationCode` | Nein         | Füllt den Aktivierungscode des Terminals vorab aus.                                                                                 |

## Integration testen [#test-the-integration]

1. Initialisieren Sie mit `isProduction = false` (Standard); Sandbox-Zahlungen bewegen nie echtes Geld.
2. Installieren Sie das Tapaya-Terminal-Plugin auf Ihrem Testgerät, melden Sie sich mit einem Sandbox-Händler an und aktivieren Sie es.
3. Führen Sie eine Zahlung über einen kleinen Betrag aus und prüfen Sie den `Success`-Ablauf von Anfang bis Ende, einschließlich Ihrer Beleganzeige.
4. Testen Sie die Fehlerpfade: Lehnen Sie eine Zahlung am Terminal ab, brechen Sie eine ab und starten Sie eine in einer nicht unterstützten
   Währung; Ihre Behandlung von `PayResult`/`CreationFailed` sollte jeden dieser Fälle abdecken.

Wenn Sie bereit sind, live zu gehen, setzen Sie in `initialize` den Wert `isProduction = true` und authentifizieren Sie sich mit Zugangsdaten
für die Produktion. Der Zahlungscode ändert sich nicht.
