# Cloud Accept API (/fr/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>

La Cloud Accept API permet à votre caisse enregistreuse électronique (ECR) ou à votre backend POS de lancer un paiement sur un appareil Tapaya
via le cloud. Votre système ne communique jamais directement avec l’appareil : il envoie une demande de paiement à Tapaya, Tapaya
avertit l’appareil appairé, et l’appareil affiche l’écran de paiement au client. La progression vous parvient
sous forme de webhooks, ou vous l’obtenez par interrogation régulière (polling).

## Choisir comment appairer les appareils [#choose-how-to-pair-devices]

Chaque appareil auquel vous envoyez des paiements a besoin d’un `externalDeviceId` : le nom que votre système lui donne, par exemple
`till-01`. Attribuez-le de l’une des deux manières suivantes :

<Cards>
  <VisualCard title="Depuis votre application" visual="<Layers />" href="#assign-from-your-app" description="Votre application attribue l’externalDeviceId via l’Accept SDK sur l’appareil." />

  <VisualCard title="QR code depuis la plateforme" visual="<QrCode />" href="#scan-a-qr-code" description="Le commerçant affiche un QR code dans la Tapaya Platform et le scanne avec l’application commerçant." />
</Cards>

## Avant de commencer [#before-you-start]

1. **Obtenez votre Server Secret Token** sur la [page API Keys](https://platform.tapaya.com/api-keys/). Vous
   l’échangez ci-dessous contre un jeton d’accès limité à un commerçant (voir [l’authentification de la Platform API](/platform-api#authentication)).
2. **Enregistrez le commerçant** et conservez son `merchantToken`. Voir
   [Enregistrer un nouveau commerçant](./backend_integration#register-a-new-merchant).
3. **Préparez chaque appareil** avec l’application Tapaya connectée pour ce commerçant, afin qu’il puisse être appairé et recevoir
   des demandes.

<Callout title="Un jeton, un commerçant" type="info">
  Chaque appel à la Cloud Accept API est limité à un seul et unique commerçant. Pour servir plusieurs commerçants, échangez un
  jeton d’accès distinct pour chacun d’eux.
</Callout>

<section id="cloud-accept-steps">
  <div className="fd-steps [&_h3]:fd-step">
    ### Appairer un appareil [#pair-a-device]

    L’appairage attribue un `externalDeviceId` à un appareil Tapaya, afin que vous puissiez le retrouver lorsque vous listez les appareils
    et le cibler lorsque vous lancez un paiement.

    #### Attribuer depuis votre application [#assign-from-your-app]

    Votre application appelle une fonction de l’Accept SDK sur l’appareil pour lui attribuer son `externalDeviceId`. Utilisez
    le même identifiant dans chaque appel à la Cloud Accept API concernant cet appareil.

    #### Scanner un QR code [#scan-a-qr-code]

    Le commerçant ouvre le parcours d’appairage dans la Tapaya Platform, qui affiche un QR code pour
    l’`externalDeviceId` à attribuer. Le commerçant le scanne avec l’application commerçant Tapaya sur l’appareil
    cible, et l’application finalise l’appairage.

    ### Échanger un jeton d’accès [#exchange-an-access-token]

    Échangez votre Server Secret Token et le `merchantToken` du commerçant contre un jeton d’accès avec
    `POST /ecr/auth`. Le `merchantToken` n’est envoyé qu’ici ; tous les appels suivants envoient uniquement
    `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  | Description                               |
    | ----- | ----------------------------------------- |
    | `401` | Jeton API invalide ou expiré.             |
    | `403` | L’accès au commerçant n’est pas autorisé. |

    **Renouvelez le jeton avant `expiresAt`.** Appelez `POST /ecr/auth/refresh` avec l’`accessToken` actuel, encore valide,
    dans l’en-tête `Authorization`. L’appel renvoie le même corps avec un nouveau jeton ; enregistrez-le
    et utilisez-le désormais.

    <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  | Description                                                                                     |
    | ----- | ----------------------------------------------------------------------------------------------- |
    | `401` | Le jeton d’accès est invalide, expiré ou révoqué. Échangez-en un nouveau avec `POST /ecr/auth`. |
    | `403` | Vous n’avez plus accès au commerçant. Aucun nouveau jeton n’est émis.                           |

    ### Trouver un appareil prêt [#find-a-ready-device]

    Listez les appareils du commerçant avec `GET /ecr/devices`. Ajoutez `?connected=true` pour exclure
    les appareils déconnectés, ou récupérez un seul appareil avec `GET /ecr/devices/{externalDeviceId}`.

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

    N’envoyez des paiements qu’à un appareil dont l’`availability` vaut `ready`. La liste n’est qu’un instantané :
    la disponibilité est vérifiée de nouveau lors de l’envoi, si bien qu’un appareil `ready` peut tout de même refuser
    une demande. Voir [Disponibilité des appareils](#device-availability).

    ### Envoyer une demande de paiement [#submit-a-payment-request]

    Envoyez le montant à un appareil avec `POST /ecr/payment-requests`. Tapaya enregistre la demande et
    avertit l’appareil, qui affiche l’écran de paiement.

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

    **Paramètres :**

    | Champ              | Type      | Obligatoire | Description                                                                      |
    | ------------------ | --------- | ----------- | -------------------------------------------------------------------------------- |
    | `externalDeviceId` | `string`  | ✓           | L’appareil appairé qui doit afficher le paiement.                                |
    | `amount`           | `integer` | ✓           | Montant dans la plus petite unité de la devise (`12500` correspond à 125,00 Kč). |
    | `tipAmount`        | `integer` |             | Pourboire dans la plus petite unité de la devise.                                |
    | `currency`         | `string`  | ✓           | Code de devise ISO 4217.                                                         |
    | `paymentToken`     | `string`  |             | Un paiement existant auquel rattacher la demande.                                |

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

    <Callout title="202 n’est pas un résultat de paiement" type="warn">
      `202 Accepted` signifie que Tapaya a enregistré la demande. Cela ne signifie pas que l’appareil l’a vue
      ni que le client a payé. Tout ce qui échoue ensuite vous parvient sous forme de changement de statut.
    </Callout>

    **Sécurisez les nouvelles tentatives.** L’en-tête `Idempotency-Key` est obligatoire. Réutiliser une clé avec le même corps
    renvoie la demande d’origine au lieu de lancer un second paiement ; la réutiliser avec un corps différent
    renvoie `409 idempotency_conflict`. Associez la clé à la vente et à la tentative, comme dans
    `order-1042-attempt-1`.

    ### Suivre le résultat [#track-the-result]

    Suivez la demande à travers ses [statuts](#request-statuses) avec des webhooks, ou interrogez régulièrement
    `GET /ecr/payment-requests/{requestId}`. Dès qu’elle atteint `payment_created`, la réponse contient
    le `paymentToken`, et les événements `payment.updated` rendent compte de la progression du paiement lui-même.

    Pour recevoir des webhooks, enregistrez votre endpoint HTTPS avec `POST /ecr/webhook-subscriptions`, ou définissez
    l’URL de callback et les types d’événements du commerçant dans la Tapaya Platform.

    <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="Enregistrez le signing secret" type="warn">
      `signingSecret` n’est renvoyé qu’à la création de l’abonnement. Conservez-le en lieu sûr ; vous
      en avez besoin pour vérifier la signature des webhooks entrants.
    </Callout>

    Un abonnement appartient à l’intégrateur et au commerçant, et non à un jeton d’accès ; il est donc conservé
    lors du renouvellement du jeton. Répondez à chaque événement par un `2xx` dès que vous l’avez accepté.

    <Callout title="Vérifiez avant de clôturer la vente" type="error">
      Les webhooks sont des notifications, et non une preuve de paiement ; un webhook peut arriver en retard, ou ne jamais arriver. Ne
      clôturez jamais une vente sur la seule base d’un webhook :

      * **Lorsqu’un webhook arrive,** confirmez le résultat final via l’API, par exemple avec
        [Récupérer les détails d’un paiement](/platform-api/reporting#retrieve-payment-details).
      * **Lorsqu’aucun webhook n’arrive** dans un délai adapté à votre parcours d’encaissement, et au plus tard
        à l’`expiresAt` de la demande, appelez `GET /ecr/payment-requests/{requestId}` pour lire son
        statut actuel, puis confirmez le paiement via l’API comme ci-dessus.
    </Callout>
  </div>
</section>

## Référence [#reference]

### Endpoints [#endpoints]

| Méthode  | Endpoint                                      | Description                                                                                                         |
| -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `POST`   | `/ecr/auth`                                   | Échange votre jeton API et un `merchantToken` contre un jeton d’accès.                                              |
| `POST`   | `/ecr/auth/refresh`                           | Renouvelle un jeton d’accès encore valide.                                                                          |
| `GET`    | `/ecr/devices`                                | Liste les appareils du commerçant. Filtre `connected=true` facultatif.                                              |
| `GET`    | `/ecr/devices/{externalDeviceId}`             | Renvoie un appareil et sa disponibilité actuelle.                                                                   |
| `POST`   | `/ecr/payment-requests`                       | Enregistre une demande de paiement et avertit l’appareil. Nécessite `Idempotency-Key`.                              |
| `GET`    | `/ecr/payment-requests/{requestId}`           | Renvoie le statut de la demande, l’éventuel motif de refus et le `paymentToken`.                                    |
| `POST`   | `/ecr/webhook-subscriptions`                  | Enregistre une URL HTTPS et des types d’événements. Renvoie le signing secret (secret de signature) une seule fois. |
| `GET`    | `/ecr/webhook-subscriptions`                  | Liste vos abonnements pour le commerçant, sans les signing secrets.                                                 |
| `DELETE` | `/ecr/webhook-subscriptions/{subscriptionId}` | Supprime un abonnement.                                                                                             |

### Disponibilité des appareils [#device-availability]

Les deux endpoints d’appareils renvoient `externalDeviceId`, `name`, `connected`, `availability`, `lastSeenAt` et
`updatedAt`.

| Valeur        | Description                                                                   |
| ------------- | ----------------------------------------------------------------------------- |
| `ready`       | Connecté, et capable de recevoir et d’afficher une demande.                   |
| `busy`        | Traite un paiement ou détient une demande active.                             |
| `unavailable` | Déconnecté, sans session valide, ou incapable d’afficher l’écran de paiement. |

### Statuts des demandes [#request-statuses]

| Statut            | Description                                                                                    |
| ----------------- | ---------------------------------------------------------------------------------------------- |
| `pending`         | Enregistrée et en attente d’acceptation par l’appareil.                                        |
| `acknowledged`    | L’appareil l’a reçue ; le paiement n’existe peut-être pas encore.                              |
| `payment_created` | Un paiement et son `paymentToken` existent. Suivez la suite de la progression sur le paiement. |
| `rejected`        | L’appareil ou Tapaya a refusé la demande.                                                      |

### Erreurs [#errors]

| Code          | Motif                  | Description                                              |
| ------------- | ---------------------- | -------------------------------------------------------- |
| `403` / `404` | Objet indisponible     | Vous n’avez pas l’autorisation, ou l’objet n’existe pas. |
| `409`         | `device_busy`          | L’appareil a déjà une demande ou un paiement actif.      |
| `409`         | `device_unavailable`   | L’appareil est connecté mais n’est pas prêt.             |
| `409`         | `idempotency_conflict` | La clé a déjà été utilisée avec un corps différent.      |
| `503`         | `device_offline`       | L’appareil n’est pas connecté.                           |

Les erreurs de validation du montant et de la devise suivent les mêmes conventions que le reste de la
[Platform API](/platform-api).

### Événements webhook [#webhook-events]

| Événement                     | Envoyé lorsque                                                                          | Contenu de `data`                                                                                                     |
| ----------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `device.connected`            | Un appareil ouvre une nouvelle connexion active.                                        | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                         |
| `device.disconnected`         | La connexion se ferme ou la vérification d’activité expire.                             | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                         |
| `device.availability_changed` | La disponibilité passe de l’une à l’autre des valeurs `ready`, `busy` et `unavailable`. | `externalDeviceId`, `availability`, `reason` facultatif                                                               |
| `payment_request.updated`     | La demande est enregistrée, reçue, refusée, expire ou se voit associer un paiement.     | `requestId`, `externalDeviceId`, `status`, `paymentToken`, `reference`, `reason`, `expiresAt`                         |
| `payment.updated`             | Le statut du paiement change.                                                           | `paymentToken`, `requestId`, `externalDeviceId`, `status`, `reference`, `amount`, `currency`, `receiptUrl` facultatif |

Chaque événement arrive sous forme de `POST` HTTP dans la même enveloppe :

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