# Cloud Accept API (/de/in-store/integration/cloud_accept_api)





<div className="bg-background relative h-auto w-full overflow-hidden rounded-lg border mt-8">
  <FlickeringGrid className="absolute inset-0 z-0 size-full" squareSize="4" gridGap="6" maxOpacity="0.1" flickerChance="0.1" height="800" width="2000" />

  <CloudAcceptDiagram />
</div>

Mit der Cloud Accept API kann Ihre elektronische Registrierkasse (ECR) oder Ihr POS-Backend über die Cloud eine Zahlung auf einem Tapaya-Gerät
starten. Ihr System kommuniziert nie direkt mit dem Gerät: Es sendet eine Zahlungsanfrage an Tapaya, Tapaya
benachrichtigt das gekoppelte Gerät, und das Gerät zeigt dem Kunden den Zahlungsbildschirm. Den Fortschritt erhalten Sie
als Webhooks, oder Sie fragen ihn regelmäßig ab (Polling).

## Art der Gerätekopplung wählen [#choose-how-to-pair-devices]

Jedes Gerät, an das Sie Zahlungen senden, benötigt eine `externalDeviceId`: den Namen, unter dem Ihr System es kennt, zum Beispiel
`till-01`. Sie können sie auf eine von zwei Arten zuweisen:

<Cards>
  <VisualCard title="Aus Ihrer App" visual="<Layers />" href="#assign-from-your-app" description="Ihre App weist die externalDeviceId über das Accept SDK auf dem Gerät zu." />

  <VisualCard title="QR-Code aus der Plattform" visual="<QrCode />" href="#scan-a-qr-code" description="Der Händler zeigt einen QR-Code in der Tapaya Platform an und scannt ihn mit der Händler-App." />
</Cards>

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

1. **Holen Sie sich Ihr Server Secret Token** auf der [Seite API Keys](https://platform.tapaya.com/api-keys/). Weiter unten tauschen
   Sie es gegen ein auf den Händler beschränktes Zugriffstoken ein (siehe [Authentifizierung der Platform API](/platform-api#authentication)).
2. **Registrieren Sie den Händler** und bewahren Sie sein `merchantToken` auf. Siehe
   [Neuen Händler registrieren](./backend_integration#register-a-new-merchant).
3. **Bereiten Sie jedes Gerät vor**, indem Sie die Tapaya-App für diesen Händler anmelden, damit es gekoppelt werden und
   Anfragen empfangen kann.

<Callout title="Ein Token, ein Händler" type="info">
  Jeder Aufruf der Cloud Accept API ist auf genau einen Händler beschränkt. Wenn Sie mehrere Händler bedienen, tauschen Sie
  für jeden ein eigenes Zugriffstoken ein.
</Callout>

<section id="cloud-accept-steps">
  <div className="fd-steps [&_h3]:fd-step">
    ### Gerät koppeln [#pair-a-device]

    Die Kopplung weist einem Tapaya-Gerät eine `externalDeviceId` zu, sodass Sie es beim Auflisten der Geräte finden
    und beim Starten einer Zahlung gezielt ansprechen können.

    #### Zuweisung aus Ihrer App [#assign-from-your-app]

    Ihre Anwendung ruft auf dem Gerät eine Funktion des Accept SDK auf, die ihm seine `externalDeviceId` zuweist. Verwenden Sie
    in jedem Aufruf der Cloud Accept API für dieses Gerät dieselbe Kennung.

    #### QR-Code scannen [#scan-a-qr-code]

    Der Händler öffnet den Kopplungsvorgang in der Tapaya Platform, die einen QR-Code für die
    zuzuweisende `externalDeviceId` anzeigt. Der Händler scannt ihn mit der Tapaya-Händler-App auf dem Zielgerät,
    und die App schließt die Kopplung ab.

    ### Zugriffstoken eintauschen [#exchange-an-access-token]

    Tauschen Sie Ihr Server Secret Token und das `merchantToken` des Händlers mit
    `POST /ecr/auth` gegen ein Zugriffstoken ein. `merchantToken` wird nur hier gesendet; alle späteren Aufrufe senden nur
    `Authorization: Bearer <accessToken>`.

    <CodeBlockTabs defaultValue="curl" groupId="cloud-accept-client">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="curl">
          cURL
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="typescript">
          TypeScript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="curl">
        ```bash
        curl -X 'POST' 'https://api.tapaya.com/ecr/auth' \
        -H 'Content-Type: application/json' \
        -H 'Authorization: Bearer REPLACE_ME' \
        -d '{
                "merchantToken": "unique_merchant_id_from_your_db"
            }'
        ```
      </CodeBlockTab>

      <CodeBlockTab value="typescript">
        ```typescript
        const res = await fetch('https://api.tapaya.com/ecr/auth', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${process.env.TAPAYA_SECRET_TOKEN}`,
            },
            body: JSON.stringify({ merchantToken: 'unique_merchant_id_from_your_db' }),
        });
        const { accessToken, expiresAt } = await res.json();
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ```json title="200 OK"
    {
        "accessToken": "...",
        "tokenType": "Bearer",
        "expiresAt": "2026-01-13T16:30:59Z"
    }
    ```

    | Code  | Beschreibung                            |
    | ----- | --------------------------------------- |
    | `401` | Ungültiges oder abgelaufenes API-Token. |
    | `403` | Zugriff auf den Händler nicht erlaubt.  |

    **Erneuern Sie das Token vor `expiresAt`.** Rufen Sie `POST /ecr/auth/refresh` mit dem aktuellen, noch gültigen
    `accessToken` im Header `Authorization` auf. Der Endpoint gibt denselben Body mit einem neuen Token zurück; speichern Sie es
    und verwenden Sie ab dann nur noch dieses.

    <CodeBlockTabs defaultValue="curl" groupId="cloud-accept-client">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="curl">
          cURL
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="typescript">
          TypeScript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="curl">
        ```bash
        curl -X 'POST' 'https://api.tapaya.com/ecr/auth/refresh' \
        -H 'Authorization: Bearer ACCESS_TOKEN'
        ```
      </CodeBlockTab>

      <CodeBlockTab value="typescript">
        ```typescript
        const res = await fetch('https://api.tapaya.com/ecr/auth/refresh', {
            method: 'POST',
            headers: { Authorization: `Bearer ${accessToken}` },
        });
        const renewed = await res.json(); // { accessToken, tokenType, expiresAt }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    | Code  | Beschreibung                                                                                                 |
    | ----- | ------------------------------------------------------------------------------------------------------------ |
    | `401` | Das Zugriffstoken ist ungültig, abgelaufen oder widerrufen. Tauschen Sie mit `POST /ecr/auth` ein neues ein. |
    | `403` | Sie haben keinen Zugriff mehr auf den Händler. Es wird kein neues Token ausgestellt.                         |

    ### Einsatzbereites Gerät finden [#find-a-ready-device]

    Listen Sie die Geräte des Händlers mit `GET /ecr/devices` auf. Mit `?connected=true` lassen Sie
    nicht verbundene Geräte weg; ein einzelnes Gerät rufen Sie mit `GET /ecr/devices/{externalDeviceId}` ab.

    <CodeBlockTabs defaultValue="curl" groupId="cloud-accept-client">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="curl">
          cURL
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="typescript">
          TypeScript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="curl">
        ```bash
        curl 'https://api.tapaya.com/ecr/devices/till-01' \
        -H 'Authorization: Bearer ACCESS_TOKEN'
        ```
      </CodeBlockTab>

      <CodeBlockTab value="typescript">
        ```typescript
        const res = await fetch('https://api.tapaya.com/ecr/devices/till-01', {
            headers: { Authorization: `Bearer ${accessToken}` },
        });
        const device = await res.json();
        const canPay = device.availability === 'ready';
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ```json title="200 OK"
    {
        "externalDeviceId": "till-01",
        "name": "Front counter",
        "connected": true,
        "availability": "ready",
        "lastSeenAt": "2026-01-13T16:30:59Z",
        "updatedAt": "2026-01-13T16:30:59Z"
    }
    ```

    Senden Sie Zahlungen nur an ein Gerät, dessen `availability` den Wert `ready` hat. Die Liste ist eine Momentaufnahme:
    Die Verfügbarkeit wird beim Absenden erneut geprüft, daher kann auch ein Gerät mit `ready` eine Anfrage
    ablehnen. Siehe [Geräteverfügbarkeit](#device-availability).

    ### Zahlungsanfrage senden [#submit-a-payment-request]

    Senden Sie den Betrag mit `POST /ecr/payment-requests` an ein Gerät. Tapaya speichert die Anfrage und
    benachrichtigt das Gerät, das den Zahlungsbildschirm anzeigt.

    <CodeBlockTabs defaultValue="curl" groupId="cloud-accept-client">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="curl">
          cURL
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="typescript">
          TypeScript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="curl">
        ```bash
        curl -X 'POST' 'https://api.tapaya.com/ecr/payment-requests' \
        -H 'Content-Type: application/json' \
        -H 'Authorization: Bearer ACCESS_TOKEN' \
        -H 'Idempotency-Key: order-1042-attempt-1' \
        -d '{
                "externalDeviceId": "till-01",
                "amount": 12500,
                "tipAmount": 500,
                "currency": "CZK"
            }'
        ```
      </CodeBlockTab>

      <CodeBlockTab value="typescript">
        ```typescript
        const res = await fetch('https://api.tapaya.com/ecr/payment-requests', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${accessToken}`,
                'Idempotency-Key': 'order-1042-attempt-1',
            },
            body: JSON.stringify({
                externalDeviceId: 'till-01',
                amount: 12500, // 125.00 Kč
                tipAmount: 500, // 5.00 Kč
                currency: 'CZK',
            }),
        });
        const { requestId } = await res.json(); // 202 Accepted
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    **Parameter:**

    | Feld               | Typ       | Erforderlich | Beschreibung                                                                |
    | ------------------ | --------- | ------------ | --------------------------------------------------------------------------- |
    | `externalDeviceId` | `string`  | ✓            | Das gekoppelte Gerät, das die Zahlung anzeigen soll.                        |
    | `amount`           | `integer` | ✓            | Betrag in der kleinsten Einheit der Währung (`12500` entspricht 125,00 Kč). |
    | `tipAmount`        | `integer` |              | Trinkgeld in der kleinsten Einheit der Währung.                             |
    | `currency`         | `string`  | ✓            | ISO-4217-Währungscode.                                                      |
    | `paymentToken`     | `string`  |              | Eine bestehende Zahlung, an die die Anfrage angehängt werden soll.          |

    ```json title="202 Accepted"
    {
        "requestId": "...",
        "status": "pending",
        "paymentToken": null,
        "expiresAt": "2026-01-13T16:30:59Z"
    }
    ```

    <Callout title="202 ist kein Zahlungsergebnis" type="warn">
      `202 Accepted` bedeutet, dass Tapaya die Anfrage gespeichert hat. Es bedeutet weder, dass das Gerät sie gesehen hat,
      noch, dass der Kunde bezahlt hat. Alles, was später fehlschlägt, erreicht Sie als Statusänderung.
    </Callout>

    **Sichern Sie Wiederholungsversuche ab.** Der Header `Idempotency-Key` ist erforderlich. Wird ein Schlüssel mit demselben Body erneut verwendet,
    wird die ursprüngliche Anfrage zurückgegeben, statt eine zweite Zahlung zu starten; wird er mit einem anderen
    Body erneut verwendet, wird `409 idempotency_conflict` zurückgegeben. Binden Sie den Schlüssel an den Verkauf und den Versuch, wie bei
    `order-1042-attempt-1`.

    ### Ergebnis verfolgen [#track-the-result]

    Verfolgen Sie die Anfrage durch ihre [Status](#request-statuses) mit Webhooks, oder fragen Sie regelmäßig
    `GET /ecr/payment-requests/{requestId}` ab. Sobald sie `payment_created` erreicht, enthält die Antwort
    das `paymentToken`, und Events vom Typ `payment.updated` melden den Fortschritt der Zahlung selbst.

    Um Webhooks zu empfangen, registrieren Sie Ihren HTTPS-Endpoint mit `POST /ecr/webhook-subscriptions` oder legen Sie die
    Callback-URL und die Event-Typen für den Händler in der Tapaya Platform fest.

    <CodeBlockTabs defaultValue="curl" groupId="cloud-accept-client">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="curl">
          cURL
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="typescript">
          TypeScript
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="curl">
        ```bash
        curl -X 'POST' 'https://api.tapaya.com/ecr/webhook-subscriptions' \
        -H 'Content-Type: application/json' \
        -H 'Authorization: Bearer ACCESS_TOKEN' \
        -d '{
                "url": "https://integrator.example.com/webhooks/tapaya",
                "events": ["payment_request.updated", "payment.updated"]
            }'
        ```
      </CodeBlockTab>

      <CodeBlockTab value="typescript">
        ```typescript
        const res = await fetch('https://api.tapaya.com/ecr/webhook-subscriptions', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                Authorization: `Bearer ${accessToken}`,
            },
            body: JSON.stringify({
                url: 'https://integrator.example.com/webhooks/tapaya',
                events: ['payment_request.updated', 'payment.updated'],
            }),
        });
        const { subscriptionId, signingSecret } = await res.json(); // 201 Created
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    ```json title="201 Created"
    {
        "subscriptionId": "...",
        "url": "https://integrator.example.com/webhooks/tapaya",
        "events": ["payment_request.updated", "payment.updated"],
        "signingSecret": "..."
    }
    ```

    <Callout title="Signing secret speichern" type="warn">
      `signingSecret` wird nur beim Erstellen des Abonnements zurückgegeben. Speichern Sie es sicher; Sie
      benötigen es, um die Signatur eingehender Webhooks zu prüfen.
    </Callout>

    Ein Abonnement gehört dem Integrator und dem Händler, nicht einem Zugriffstoken, und bleibt daher bei der
    Erneuerung des Tokens bestehen. Beantworten Sie jedes Event mit einem `2xx`, sobald Sie es angenommen haben.

    <Callout title="Vor dem Abschluss des Verkaufs prüfen" type="error">
      Webhooks sind Benachrichtigungen, kein Zahlungsnachweis, und ein Webhook kann verspätet oder gar nicht ankommen. Schließen Sie
      einen Verkauf nie allein aufgrund eines Webhooks ab:

      * **Wenn ein Webhook ankommt,** bestätigen Sie das endgültige Ergebnis über die API, zum Beispiel mit
        [Zahlungsdetails abrufen](/platform-api/reporting#retrieve-payment-details).
      * **Wenn kein Webhook ankommt** (innerhalb eines zu Ihrem Bezahlvorgang passenden Timeouts, spätestens jedoch
        bis zum `expiresAt` der Anfrage), rufen Sie `GET /ecr/payment-requests/{requestId}` auf, um ihren
        aktuellen Status abzufragen, und bestätigen Sie die Zahlung dann wie oben beschrieben über die API.
    </Callout>
  </div>
</section>

## Referenz [#reference]

### Endpoints [#endpoints]

| Methode  | Endpoint                                      | Beschreibung                                                                                               |
| -------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `POST`   | `/ecr/auth`                                   | Tauscht Ihr API-Token und ein `merchantToken` gegen ein Zugriffstoken ein.                                 |
| `POST`   | `/ecr/auth/refresh`                           | Erneuert ein noch gültiges Zugriffstoken.                                                                  |
| `GET`    | `/ecr/devices`                                | Listet die Geräte des Händlers auf. Optionaler Filter `connected=true`.                                    |
| `GET`    | `/ecr/devices/{externalDeviceId}`             | Gibt ein Gerät und seine aktuelle Verfügbarkeit zurück.                                                    |
| `POST`   | `/ecr/payment-requests`                       | Speichert eine Zahlungsanfrage und benachrichtigt das Gerät. Erfordert `Idempotency-Key`.                  |
| `GET`    | `/ecr/payment-requests/{requestId}`           | Gibt den Status der Anfrage, einen etwaigen Ablehnungsgrund und das `paymentToken` zurück.                 |
| `POST`   | `/ecr/webhook-subscriptions`                  | Registriert eine HTTPS-URL und Event-Typen. Gibt das signing secret (Signaturschlüssel) nur einmal zurück. |
| `GET`    | `/ecr/webhook-subscriptions`                  | Listet Ihre Abonnements für den Händler auf, ohne signing secrets.                                         |
| `DELETE` | `/ecr/webhook-subscriptions/{subscriptionId}` | Entfernt ein Abonnement.                                                                                   |

### Geräteverfügbarkeit [#device-availability]

Beide Geräte-Endpoints geben `externalDeviceId`, `name`, `connected`, `availability`, `lastSeenAt` und
`updatedAt` zurück.

| Wert          | Beschreibung                                                                                     |
| ------------- | ------------------------------------------------------------------------------------------------ |
| `ready`       | Verbunden und in der Lage, eine Anfrage zu empfangen und anzuzeigen.                             |
| `busy`        | Verarbeitet gerade eine Zahlung oder hat eine aktive Anfrage.                                    |
| `unavailable` | Nicht verbunden, ohne gültige Session oder nicht in der Lage, den Zahlungsbildschirm anzuzeigen. |

### Status der Anfrage [#request-statuses]

| Status            | Beschreibung                                                                                               |
| ----------------- | ---------------------------------------------------------------------------------------------------------- |
| `pending`         | Gespeichert; wartet darauf, dass das Gerät sie annimmt.                                                    |
| `acknowledged`    | Das Gerät hat sie empfangen; die Zahlung existiert möglicherweise noch nicht.                              |
| `payment_created` | Eine Zahlung und ihr `paymentToken` existieren. Verfolgen Sie den weiteren Fortschritt anhand der Zahlung. |
| `rejected`        | Das Gerät oder Tapaya hat die Anfrage abgelehnt.                                                           |

### Fehler [#errors]

| Code          | Grund                  | Beschreibung                                                   |
| ------------- | ---------------------- | -------------------------------------------------------------- |
| `403` / `404` | Objekt nicht verfügbar | Ihnen fehlt die Berechtigung, oder das Objekt existiert nicht. |
| `409`         | `device_busy`          | Das Gerät hat bereits eine aktive Anfrage oder Zahlung.        |
| `409`         | `device_unavailable`   | Das Gerät ist verbunden, aber nicht bereit.                    |
| `409`         | `idempotency_conflict` | Der Schlüssel wurde bereits mit einem anderen Body verwendet.  |
| `503`         | `device_offline`       | Das Gerät ist nicht verbunden.                                 |

Validierungsfehler bei Betrag und Währung folgen denselben Konventionen wie der Rest der
[Platform API](/platform-api).

### Webhook-Events [#webhook-events]

| Event                         | Gesendet, wenn                                                                                         | Inhalt von `data`                                                                                                   |
| ----------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `device.connected`            | Ein Gerät eine neue Live-Verbindung aufbaut.                                                           | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                       |
| `device.disconnected`         | Die Verbindung geschlossen wird oder bei der Verbindungsprüfung (Liveness-Check) ein Timeout auftritt. | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                       |
| `device.availability_changed` | Die Verfügbarkeit zwischen `ready`, `busy` und `unavailable` wechselt.                                 | `externalDeviceId`, `availability`, optional `reason`                                                               |
| `payment_request.updated`     | Die Anfrage wird gespeichert, bestätigt oder zurückgewiesen, läuft ab oder erhält eine Zahlung.        | `requestId`, `externalDeviceId`, `status`, `paymentToken`, `reference`, `reason`, `expiresAt`                       |
| `payment.updated`             | Sich der Status der Zahlung ändert.                                                                    | `paymentToken`, `requestId`, `externalDeviceId`, `status`, `reference`, `amount`, `currency`, optional `receiptUrl` |

Jedes Event kommt als HTTP-`POST` mit derselben Hülle (Envelope) an:

```json
{
    "eventId": "...",
    "type": "payment_request.updated",
    "occurredAt": "2026-01-13T16:30:59Z",
    "data": {
        "requestId": "...",
        "externalDeviceId": "till-01",
        "status": "acknowledged",
        "paymentToken": null,
        "reference": "ORDER-1042",
        "reason": null,
        "expiresAt": "2026-01-13T16:30:59Z"
    }
}
```
