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



Platzieren Sie sichere Kartenfelder in Ihrem eigenen Checkout. Formular und Absende-Button gehören Ihnen; Tapaya übernimmt die
Validierung der Karte, die Tokenisierung und 3-D Secure. Eingebettete Zahlungen sind für freigegebene Händler live, zum Testen steht die Sandbox
zur Verfügung. Dieser Leitfaden verwendet Tapaya.js **0.2.0** und das Node.js SDK **0.3.0**.

<EmbeddedPaymentFlowDiagram />

<Callout title="Zahlungsmethoden" type="info">
  Embedded Checkout unterstützt Karten, Apple Pay und Google Pay. Verwenden Sie für Karten Ihren eigenen Bezahl-Button und für Apple Pay und Google Pay die offiziellen
  Wallet-Buttons. Siehe [Apple Pay und Google Pay hinzufügen](#add-apple-pay-and-google-pay).
</Callout>

## Bevor Sie beginnen [#before-you-start]

### SDK installieren [#install-the-sdk]

Ein einziges Paket enthält den Server-Client, das Browser-SDK und den React-Hook:

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

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

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

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

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

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

| Import                       | Wo                                                                                                                                  |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `@tapayadot/checkout`        | Nur auf Ihrem Server. Ein Import in einem Browser-Build löst einen Fehler aus, sodass Ihr API-Schlüssel nie auf eine Seite gelangt. |
| `@tapayadot/checkout/client` | Ihre Checkout-Seite.                                                                                                                |
| `@tapayadot/checkout/react`  | Ihre Checkout-Seite, wenn Sie React verwenden.                                                                                      |

### Server konfigurieren [#configure-your-server]

Eingebettete Zahlungen benötigen keine Rückkehr-URLs. Die Serverbeispiele unten gibt es in den Varianten &#x2A;*HTTP (fetch)**, **Node.js SDK**
und **cURL**. Alle rufen dieselbe Händler-API auf und lassen die Kartenfelder im Browser unverändert.

|                    | Node.js SDK                                                                                                                                                                  | HTTP (fetch) und cURL                                                                                                  |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Schlüssel**      | `TAPAYA_SECRET_KEY`, gelesen von `new Tapaya()`                                                                                                                              | `TAPAYA_CHECKOUT_API_KEY`                                                                                              |
| **Umgebung**       | `TAPAYA_ENVIRONMENT=sandbox` oder `production`. Ohne diese Variable wählt ein `tapaya_live_`-Schlüssel die Produktion, andere Schlüssel verwenden standardmäßig die Sandbox. | `TAPAYA_CHECKOUT_API_URL`: `https://api.sandbox.tapaya.com` oder `https://api.tapaya.com`                              |
| **Plattformmodus** | Übergeben Sie das `merchantToken` der Bestellung bei jedem Aufruf oder setzen Sie es mit `new Tapaya(key, { merchantToken })`                                                | Senden Sie bei jeder Anfrage `X-Tapaya-Merchant-Token`. Die fetch-Hilfsfunktion liest ihn aus `TAPAYA_MERCHANT_TOKEN`. |

Ein Checkout-Händlerschlüssel benötigt nichts weiter. Verwenden Sie im Plattformmodus den Secret key der Organisation, und lassen Sie ein Backend,
das mehrere Händler bedient, das Token aus der authentifizierten Bestellung wählen. Siehe
[Checkout-Authentifizierung](/online/integration/api_integration#authentication). Client secrets sind für jede Zahlung spezifisch.

Schließen Sie das [Onboarding](/merchants/onboarding) mit Online-Zahlungen ab und prüfen Sie in den
[Checkout-Einstellungen](/online/integration/checkout_settings), ob der Händler **Ready** ist.

### Tapaya in Ihrer CSP erlauben [#allow-tapaya-in-your-csp]

Der Loader lädt Tapaya.js **0.2.0** von `https://js.tapaya.com/checkout/0.2.0/checkout.js`; die Version des Browser-Skripts
ist unabhängig von der Version des Node.js SDK. Fügen Sie den Asset-Origin von Tapaya zu Ihrer bestehenden Content Security
Policy hinzu:

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

Der Browser erhält den publishable key und das zahlungsspezifische client secret. Ihr geheimer API-Schlüssel für Händler
bleibt auf Ihrem Server, und Kartendaten sowie Skripte des Zahlungsanbieters bleiben im Frame von Tapaya. Für lokale Tests
stellen Sie Tapaya.js selbst bereit und übergeben dessen Origin an den Loader, zum Beispiel
`loadTapaya(publishableKey, { apiOrigin, origin: 'http://localhost:4191' })`.

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

1. Ihre Seite bindet die Kartenfelder von Tapaya ein und verweist sie auf einen Endpoint auf Ihrem Server.
2. Wenn die Seite lädt und wenn der Kunde zahlt, rufen die Kartenfelder diesen Endpoint auf. Er gibt die Zahlung der Bestellung
   als `clientSecret` zurück: eine Zugangsberechtigung für genau eine Zahlung, mit der die Kartenfelder die Zahlung direkt bei
   Tapaya bestätigen. Ihr Seitencode liest es nie.
3. Ihre Seite verfolgt das `outcome` der Zahlung. Wenn sie abgeschlossen ist, verifiziert Ihr Server die Zahlung, bevor Sie
   die Bestellung abwickeln.

## Eine Zahlung annehmen [#accept-a-payment]

<div className="fd-steps [&_h3]:fd-step">
  ### Browser-Einstellungen abrufen [#get-the-browser-settings]

  Rufen Sie die Browser-Einstellungen einmal auf Ihrem Server ab und fügen Sie sie Ihrer Frontend-Konfiguration hinzu.

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

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

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

    <TabsContent value="fetch">
      Halten Sie diese Hilfsfunktion auf Ihrem Server. Sie prüft HTTP-Fehler, behandelt auf Wunsch eine fehlende Zahlung der Bestellung als `null`
      und wiederholt nicht automatisch.

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

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

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

      Die API gibt `publishableKey` und `environment` zurück. Setzen Sie `apiOrigin` anhand der aufgerufenen API-URL; es ist kein Feld
      in der Antwort zur Konfiguration.
    </TabsContent>

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

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

    <TabsContent value="curl">
      Führen Sie dies auf Ihrem Server oder in einem vertrauenswürdigen Terminal aus. Fügen Sie im Plattformmodus
      `--header "X-Tapaya-Merchant-Token: $TAPAYA_MERCHANT_TOKEN"` zu jedem cURL-Befehl in diesem Leitfaden hinzu.

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

      Verwenden Sie den zurückgegebenen `publishableKey` und den Origin Ihrer `TAPAYA_CHECKOUT_API_URL` als Browser-Konfiguration.
    </TabsContent>
  </Tabs>

  Beide Werte können Sie unbedenklich in Ihre Seite aufnehmen. Publishable keys verwenden `pk_test_` mit Test-Gateway-Schlüsseln und `pk_live_`
  mit Live-Gateway-Schlüsseln. Übergeben Sie dem Loader immer den von Ihrem Server zurückgegebenen `apiOrigin`, damit Browser und Server
  in derselben Umgebung bleiben.

  ### Kartenfelder hinzufügen [#add-the-card-fields]

  Fügen Sie Ihrem bestehenden Checkout-Formular einen Container hinzu:

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

  Laden Sie Tapaya und binden Sie die Felder ein. `prepare` ist der Endpoint, den Sie im nächsten Schritt hinzufügen.

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

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

  Beim Einbinden rufen die Felder `prepare` auf, um die bestehende Zahlung der Bestellung fortzusetzen. Ein Neuladen während einer Zahlung führt nie
  zu einer erneuten Belastung: Die Seite zeigt stattdessen den aktuellen Zustand der Zahlung.

  ### Zahlung der Bestellung zurückgeben [#return-the-orders-payment]

  Verbinden Sie diesen Handler mit `POST /api/payments`. `getCheckoutOrder()` ist eine Funktion Ihrer eigenen Anwendung: Authentifizieren Sie
  den Kunden, prüfen Sie den Zugriff auf seine Bestellung und geben Sie deren gespeicherte Werte `id`, `amount`, `currency` und `status` zurück. Wenden Sie
  auf die Route Ihren üblichen CSRF-Schutz an.

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

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

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

    <TabsContent value="fetch">
      Lesen Sie die bestehende Zahlung, bevor Sie einen Versuch erstellen. Verwenden Sie eine ungeklärte Zahlung erneut und erstellen Sie einen weiteren Versuch
      nur nach einer Ablehnung oder einem Abbruch mit `retryAllowed: true`.

      ```js title="server/payments.js"
      import { paymentRequest } from './tapaya.js';
      import { getCheckoutOrder } from './orders.js';

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

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

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

      Verwenden Sie einen Währungscode in Großbuchstaben und eine gespeicherte Bestell-ID mit höchstens 200 Zeichen. Lassen Sie die Bestelldaten unverändert,
      sobald die Zahlung begonnen hat. Lesen Sie bei einem Timeout die Zahlung der Bestellung erneut, bevor Sie entscheiden, ob ein neuer Versuch nötig ist.
      Erzeugen Sie niemals einen neuen Schlüssel, nur weil eine Antwort verloren ging.
    </TabsContent>

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

      const tapaya = new Tapaya()

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

      `prepare()` gibt die Zahlung der Bestellung zurück. Die Methode erstellt den ersten Versuch, gibt einen ungeklärten Versuch unverändert zurück und
      erstellt nach einer Ablehnung den nächsten Versuch. Sie leitet die idempotency keys (Idempotenzschlüssel) selbst ab, sodass ein Aufruf bei jedem Laden der Seite und
      jedem Absenden nie eine zweite Belastung erzeugt. Frieren Sie den Gesamtbetrag ein, sobald die Zahlung begonnen hat: `prepare()` wirft
      `TapayaOrderMismatchError`, wenn Betrag oder Währung der Bestellung nicht mehr zu ihrer Zahlung passen.
    </TabsContent>

    <TabsContent value="curl">
      Suchen Sie zuerst die Zahlung der Bestellung. Diese Beispiele verwenden eine auf Ihrem Server gespeicherte Bestellung über 121,00 EUR. Ein `404` bedeutet,
      dass keine Zahlung gefunden wurde; andere HTTP-Fehler berechtigen nicht dazu, einen weiteren Versuch zu erstellen.

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

      Wenn keine Zahlung existiert und die Bestellung noch eine Zahlung annehmen kann, erstellen Sie den ersten Versuch:

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

      Verwenden Sie eine bestehende ungeklärte Zahlung erneut. Erstellen Sie nach `declined` oder `cancelled` einen weiteren Versuch nur, wenn
      `retryAllowed` gleich `true` ist. Wiederholen Sie für diesen Versuch den POST mit denselben Bestelldaten und dem Schlüssel
      `order-1001:after:<previous-payment-id>`, wobei Sie den Platzhalter durch die zurückgegebene `id` ersetzen. Verwenden Sie diesen Schlüssel für
      Wiederholungen erneut. Suchen Sie nach einem Timeout die Bestellung erneut; gehen Sie nicht davon aus, dass der POST fehlgeschlagen ist.

      cURL veranschaulicht die API-Aufrufe. Der authentifizierte Handler `POST /api/payments` Ihrer Anwendung muss
      `{ clientSecret, status }` für die Kartenfelder zurückgeben. Bilden Sie den API-Status anhand der Tabelle unten ab und senden Sie
      `clientSecret: null`, wenn Ihre Bestellung keine Zahlung mehr annimmt. Legen Sie diese Antworten niemals in gemeinsam genutzten Terminal-Logs ab.
    </TabsContent>
  </Tabs>

  Die REST-API gibt Zahlungsstatus zurück. Das Node SDK bildet sie auf den `status` ab, den die Kartenfelder erwarten; fetch- und
  cURL-Integrationen wenden dieselbe Zuordnung in ihrem eigenen prepare-Endpoint an:

  | API-`status`                                                                                            | `status` des prepare-Endpoints |
  | ------------------------------------------------------------------------------------------------------- | ------------------------------ |
  | `successful`                                                                                            | `completed`                    |
  | `declined` oder `cancelled`, mit `retryAllowed: false`                                                  | `ended`                        |
  | `ready`, `pending`, `action_required` oder eine wiederholbare Ablehnung bzw. ein wiederholbarer Abbruch | `open`                         |

  <Accordions>
    <Accordion title="Regeln für Ihren prepare-Endpoint">
      * **Übernehmen Sie den Betrag aus Ihrer gespeicherten Bestellung,** als Ganzzahl in der kleinsten Währungseinheit, niemals aus dem Browser. Frieren Sie den Gesamtbetrag ein,
        sobald die Zahlung begonnen hat.
      * **Antworten Sie auch bei abgeschlossenen Bestellungen.** Eine Bestellung, die bereits bezahlt ist oder die Ihr geplanter Job abgeschlossen hat, antwortet weiterhin
        mit dem `status` ihrer Zahlung, sodass ein Neuladen das Ergebnis statt eines Fehlers zeigt. Sie erhält nie ein client secret,
        sodass der Kunde keine Bestellung bezahlen kann, für die Sie keine Zahlung mehr annehmen.
      * **Eine erfolgreiche Zahlung pro Bestellung.** Jede Ablehnung setzt `retryAllowed`, sodass der Kunde für dieselbe Bestellung eine andere Karte
        versuchen kann. Das Limit gilt gemeinsam mit dem [Hosted Checkout](/online/integration/api_integration#one-payment-per-order).
      * **Schützen Sie das client secret.** Senden Sie es nur an den Kunden, der berechtigt ist, diese Bestellung zu bezahlen, und protokollieren Sie es nie.
      * **Rechnen Sie mit einem einfachen POST.** Die Kartenfelder senden einen POST an `prepare` auf Ihrem eigenen Origin, mit Cookies und ohne Body. Sie lehnen
        Weiterleitungen ab und wiederholen die Anfrage nie automatisch. Für eigene Header, CSRF-Tokens oder einen Request-Body übergeben Sie statt der URL eine
        Funktion, die dasselbe JSON zurückgibt: `prepare: async () => (await fetch(…)).json()`.
    </Accordion>
  </Accordions>

  ### Aus Ihrem Handler absenden [#submit-from-your-handler]

  Fügen Sie dies demselben Skript hinzu. Ihr Handler validiert Ihr Formular und ruft `submit()` auf:

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

  `submit()` ruft `prepare` für die Zahlung auf, validiert dann die Karte, führt 3-D Secure aus und bestätigt die Zahlung
  bei Tapaya.

  ### Ergebnis anzeigen [#show-the-outcome]

  `onChange` erhält den `snapshot` der Zahlung bei jeder Änderung, auch wenn die Seite mit einer Zahlung lädt, die
  bereits abgeschlossen ist. Richten Sie sich nach `snapshot.outcome`:

  | `outcome`   | Was angezeigt werden soll                                                                                                                                        |
  | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `open`      | Der Bezahl-Button, aktiviert, solange `canSubmit` true ist. Eine Ablehnung bleibt `open`, und das nächste `submit()` erhält einen neuen Versuch.                 |
  | `pending`   | Fortschritt. Das Ergebnis ist noch nicht bekannt, daher kann nichts erneut bezahlt werden. Das SDK prüft den Status automatisch, solange die Seite sichtbar ist. |
  | `completed` | Ihre Bestätigungsseite. Dort verifiziert Ihr Server die Bestellung.                                                                                              |
  | `ended`     | Ein neuer Checkout. Für diese Bestellung ist kein weiterer Versuch möglich.                                                                                      |

  Rufen Sie `payment.destroy()` auf, wenn Ihre Checkout-Ansicht ausgehängt wird. Verwenden Sie für React `usePayment()` aus
  `@tapayadot/checkout/react`. Der Hook akzeptiert dieselben Optionen plus `publishableKey` und `apiOrigin` und gibt eine
  `fieldRef` für den Container, den aktuellen `snapshot` und `submit()` zurück.

  ### Vor der Abwicklung verifizieren [#verify-before-fulfilling]

  Das Ergebnis im Browser dient der Anzeige. Lesen Sie auf Ihrer Bestätigungsseite die Zahlung der Bestellung auf Ihrem Server und wickeln Sie die Bestellung
  nur ab, wenn die Zahlung abgeschlossen ist:

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

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

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

    <TabsContent value="fetch">
      ```js title="server/confirmation.js"
      import { findPayment } from './payments.js';

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

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

      `verify()` erstellt nie eine Zahlung und gibt vor dem ersten Versuch `null` zurück. Die Methode wirft
      `TapayaOrderMismatchError`, wenn Betrag oder Währung von Ihrer Bestellung abweichen.
    </TabsContent>

    <TabsContent value="curl">
      Rufen Sie die aktuelle Zahlung der Bestellung ab, nachdem der Browser fertig ist, oder wenn Sie ein unsicheres Ergebnis prüfen:

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

      Verifizieren Sie vor der Abwicklung der Beispielbestellung `merchantOrderId: "order-1001"`, `amount: 12100`, `currency: "EUR"`
      und `status: "successful"`. Speichern Sie die zurückgegebene `id` der Zahlung mit Ihrer Bestellung. HTTP 200 allein beweist keine Zahlung.
    </TabsContent>
  </Tabs>

  Sorgen Sie dafür, dass `markOrderPaid()` die Zahlung genau einmal verbucht, auch bei wiederholten Aufrufen, und lösen Sie die Abwicklung anhand dieses
  gespeicherten Zustands aus.

  <Callout title="Bestellungen abgleichen, deren Kunden die Seite verlassen" type="warn">
    Ein Kunde kann den Tab schließen, bevor sich die Bestätigungsseite öffnet. Betreiben Sie einen geplanten Job, der für unbezahlte Bestellungen `verify()` aufruft oder
    `GET /merchant/payments?merchantOrderId=...` liest, mit denselben Prüfungen. Abgelehnte und
    abgebrochene Versuche erlauben einen neuen Versuch, daher bleibt eine unbezahlte Bestellung `open`: Geben Sie sie nach Ihrem eigenen Zeitplan frei, und
    nur, solange ihr `paymentStatus` nicht `pending` ist. Eine fehlgeschlagene Anfrage oder ein Timeout ist keine Ablehnung, halten Sie die Bestellung daher
    reserviert, bis das Ergebnis der Zahlung bekannt ist.
  </Callout>
</div>

## Apple Pay und Google Pay hinzufügen [#add-apple-pay-and-google-pay]

Wallets erscheinen im Frame von Tapaya neben den Kartenfeldern. Prüfen Sie, bevor sie angezeigt werden, ob:

* Wallets in den [Checkout-Einstellungen](/online/integration/checkout_settings#wallets-and-appearance) aktiviert sind.
* **Apple Pay:** der Shop eine [aktive Shop-Domain](/online/integration/checkout_settings#register-apple-pay-shop-domains) hat.
* **Google Pay in der Produktion:** eine Google Pay Merchant ID konfiguriert ist.
* Gerät, Browser und Wallet des Kunden die Methode unterstützen.

Übergeben Sie `wallets` mit dem Gesamtbetrag des Checkouts in der kleinsten Währungseinheit. Tapaya zeigt die verfügbaren Methoden an, nicht unterstützte oder
deaktivierte Wallets bleiben ausgeblendet. Verwenden Sie dies anstelle des Beispiels `createPayment()` nur für Karten:

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

Der im Browser angezeigte Gesamtbetrag muss von Ihrem Server stammen, der den Betrag beim Vorbereiten der Zahlung weiterhin
berechnet und validiert.

### Klicks auf Wallets [#wallet-clicks]

Der offizielle Wallet-Button öffnet durch den Klick des Kunden das native Zahlungsfenster der Wallet. `onWalletClick` validiert Ihren
Checkout und reserviert dessen Zahlung, wenn dieser Button geklickt wird, niemals beim Einbinden oder bei der Auswahl der Methode. Ohne diesen
Callback verwenden Wallets `prepare` oder `clientSecret`. Wenn Sie die Bestellung erst beim Klick reservieren, sorgen Sie dafür, dass der beim Einbinden aufgerufene
Callback `prepare` eine bestehende Bestellung fortsetzt, ohne eine neue zu erstellen.

Lassen Sie Ihren eigenen Bezahl-Button nur sichtbar, wenn `snapshot.paymentMethod` gleich `card` ist; Wallets verwenden statt `payment.submit()` ihre eigenen
Marken-Buttons. Für Ergebnisse von Wallets gelten dieselben Abläufe zur Verifizierung und Wiederherstellung wie für Karten. Das Schließen des
Zahlungsfensters der Wallet lässt den Checkout verfügbar; eine unsichere Zahlung bleibt ausstehend und verhindert eine weitere Belastung.

### Gesamtbetrag aktualisieren [#update-the-total]

Übergeben Sie `wallets: { amount, currency, disabled: true }`, solange erforderliche Checkout-Angaben unvollständig sind. Aktualisieren Sie den
Gesamtbetrag oder aktivieren Sie die Buttons, ohne neu einzubinden:

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

Übergeben Sie `wallets: null`, um Wallets auszublenden, bis der Gesamtbetrag feststeht. Tapaya verweigert die Bestätigung, wenn die vorbereitete Zahlung
vom Gesamtbetrag der Wallet abweicht. Der React-Hook `usePayment()` akzeptiert diese Optionen und aktualisiert den Gesamtbetrag, wenn sich
`wallets` ändert.

### Eigene Auswahl der Zahlungsmethode [#your-own-method-selector]

Wenn Ihre Seite die Auswahl der Zahlungsmethode selbst steuert, übergeben Sie `paymentMethod: 'card'`, `'apple_pay'` oder `'google_pay'`, um
nur diese Methode darzustellen. Eine ausgewählte, aber nicht verfügbare Wallet zeigt das Label `walletUnavailable` und meldet
`snapshot.error.code: 'wallet_unavailable'`, wenn eine andere Zahlung gefahrlos gestartet werden kann; bieten Sie in Ihrer
Auswahl eine andere Methode an. Sperren Sie die Auswahl, solange `snapshot.busy` true ist oder die Zahlung `pending` oder `action_required` ist.

### Frames und CSP [#frames-and-csp]

Das SDK fügt seinem Frame `allow="payment"` hinzu. Wenn sich Ihr Shop in einem anderen Frame befindet, muss auch dieser äußere Frame
Zahlungen erlauben. Apple Pay über verschiedene Origins hinweg (Cross-Origin) erfordert Safari 17 oder neuer. Die Skripte der Wallet-Anbieter bleiben im Frame von Tapaya, daher
benötigt Ihre Seite in ihrer CSP keine zusätzlichen Origins von Anbietern.

## Abgelaufenen Browser-Zugriff behandeln [#handle-expired-browser-access]

Das client secret läuft nach 24 Stunden ab, und `clientSecret` ist dann `null`. Eine Zahlung, die nie abgesendet wurde, wird
dann abgebrochen, und das nächste `prepare()` startet einen neuen Versuch für dieselbe Bestellung. Eine ausstehende Zahlung bleibt im Browser `pending`,
und das SDK ruft Ihren Endpoint weiter auf, bis Ihr Server ihr Ergebnis als `status` meldet. Lesezugriffe vom Server
funktionieren weiterhin, sodass Ihr geplanter Job sie in jedem Fall abschließt.

## Serverfehler diagnostizieren [#diagnose-server-errors]

Das Node SDK wirft bei abgelehnten API-Anfragen `PaymentsApiError`. Prüfen Sie dessen `status`, `code`, `message`,
`fieldErrors` und `requestId` auf Ihrem Server. Halten Sie Zugangsdaten und client secrets aus Logs heraus und zeigen Sie dem Kunden
Ihre eigenen Texte an.

## An Ihr Branding anpassen [#match-your-branding]

Die Kartenfelder verwenden die wirksame Checkout-Farbe des Händlers aus den
[Checkout-Einstellungen](/online/integration/checkout_settings#wallets-and-appearance) als Fokusakzent (`colorFocus`).
Werte, die Sie in `appearance.variables` übergeben, überschreiben sie, und es werden nur die Variablen gesendet, die Sie festlegen.
