# Cloud Accept API (/it/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 consente al Suo registratore di cassa elettronico (ECR) o al backend POS di avviare un pagamento su un dispositivo Tapaya
tramite il cloud. Il Suo sistema non comunica mai direttamente con il dispositivo: invia una richiesta di pagamento a Tapaya, Tapaya
avvisa il dispositivo associato e il dispositivo mostra al cliente la schermata di pagamento. L'avanzamento Le viene comunicato
tramite webhook, oppure può interrogarlo periodicamente (polling).

## Scegliere come associare i dispositivi [#choose-how-to-pair-devices]

Ogni dispositivo a cui invia pagamenti richiede un `externalDeviceId`: il nome con cui il Suo sistema lo identifica, ad esempio
`till-01`. Può assegnarlo in uno dei due modi seguenti:

<Cards>
  <VisualCard title="Dalla Sua app" visual="<Layers />" href="#assign-from-your-app" description="La Sua app assegna l'externalDeviceId tramite l'Accept SDK sul dispositivo." />

  <VisualCard title="Codice QR dalla piattaforma" visual="<QrCode />" href="#scan-a-qr-code" description="L'esercente mostra un codice QR nella Tapaya Platform e lo scansiona con l'app per esercenti." />
</Cards>

## Prima di iniziare [#before-you-start]

1. **Ottenga il Suo Server Secret Token** dalla [pagina API Keys](https://platform.tapaya.com/api-keys/). Più avanti lo
   scambierà con un token di accesso limitato all'esercente (consulti [Autenticazione della Platform API](/platform-api#authentication)).
2. **Registri l'esercente** e conservi il relativo `merchantToken`. Consulti
   [Registrare un nuovo esercente](./backend_integration#register-a-new-merchant).
3. **Prepari ogni dispositivo** con l'app Tapaya in cui è stato effettuato l'accesso per quell'esercente, così che possa essere associato e ricevere
   richieste.

<Callout title="Un token, un esercente" type="info">
  Ogni chiamata alla Cloud Accept API è limitata a un solo esercente. Per servire più esercenti, scambi un token di accesso
  separato per ciascuno.
</Callout>

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

    L'associazione assegna un `externalDeviceId` a un dispositivo Tapaya, così può trovarlo quando elenca i dispositivi
    e indicarlo come destinazione quando avvia un pagamento.

    #### Assegnare dalla Sua app [#assign-from-your-app]

    La Sua applicazione chiama una funzione dell'Accept SDK sul dispositivo per assegnarne l'`externalDeviceId`. Usi
    lo stesso identificatore in ogni chiamata alla Cloud Accept API per quel dispositivo.

    #### Scansionare un codice QR [#scan-a-qr-code]

    L'esercente apre la procedura di associazione nella Tapaya Platform, che mostra un codice QR per
    l'`externalDeviceId` da assegnare. L'esercente lo scansiona con l'app Tapaya per esercenti sul dispositivo
    di destinazione e l'app completa l'associazione.

    ### Ottenere un token di accesso [#exchange-an-access-token]

    Scambi il Suo Server Secret Token e il `merchantToken` dell'esercente con un token di accesso tramite
    `POST /ecr/auth`. `merchantToken` viene inviato solo qui; tutte le chiamate successive inviano soltanto
    `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"
    }
    ```

    | Codice | Descrizione                               |
    | ------ | ----------------------------------------- |
    | `401`  | Token API non valido o scaduto.           |
    | `403`  | L'accesso all'esercente non è consentito. |

    **Rinnovi prima di `expiresAt`.** Chiami `POST /ecr/auth/refresh` con l'`accessToken` attuale, ancora valido,
    nell'header `Authorization`. Restituisce lo stesso corpo con un nuovo token; lo salvi
    e da quel momento usi quello.

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

    | Codice | Descrizione                                                                                                    |
    | ------ | -------------------------------------------------------------------------------------------------------------- |
    | `401`  | Il token di accesso non è valido, è scaduto o è stato revocato. Ne ottenga uno nuovo tramite `POST /ecr/auth`. |
    | `403`  | Non ha più accesso all'esercente. Non viene emesso alcun nuovo token.                                          |

    ### Trovare un dispositivo pronto [#find-a-ready-device]

    Elenchi i dispositivi dell'esercente con `GET /ecr/devices`. Aggiunga `?connected=true` per escludere
    i dispositivi disconnessi, oppure recuperi un singolo dispositivo con `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"
    }
    ```

    Invii pagamenti solo a un dispositivo con `availability` pari a `ready`. L'elenco è un'istantanea:
    la disponibilità viene verificata di nuovo al momento dell'invio, quindi anche un dispositivo `ready` può rifiutare
    una richiesta. Consulti [Disponibilità del dispositivo](#device-availability).

    ### Inviare una richiesta di pagamento [#submit-a-payment-request]

    Invii l'importo a un dispositivo con `POST /ecr/payment-requests`. Tapaya salva la richiesta e
    avvisa il dispositivo, che mostra la schermata di pagamento.

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

    **Parametri:**

    | Campo              | Tipo      | Obbligatorio | Descrizione                                                               |
    | ------------------ | --------- | ------------ | ------------------------------------------------------------------------- |
    | `externalDeviceId` | `string`  | ✓            | Il dispositivo associato che deve mostrare il pagamento.                  |
    | `amount`           | `integer` | ✓            | Importo nell'unità minore della valuta (`12500` corrisponde a 125,00 Kč). |
    | `tipAmount`        | `integer` |              | Mancia nell'unità minore della valuta.                                    |
    | `currency`         | `string`  | ✓            | Codice valuta ISO 4217.                                                   |
    | `paymentToken`     | `string`  |              | Un pagamento esistente a cui associare la richiesta.                      |

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

    <Callout title="202 non è un esito del pagamento" type="warn">
      `202 Accepted` significa che Tapaya ha salvato la richiesta. Non significa che il dispositivo l'abbia ricevuta
      né che il cliente abbia pagato. Qualsiasi errore successivo Le arriva come cambio di stato.
    </Callout>

    **Renda sicuri i nuovi tentativi.** L'header `Idempotency-Key` è obbligatorio. Riutilizzare una chiave con lo stesso corpo
    restituisce la richiesta originale invece di avviare un secondo pagamento; riutilizzarla con un corpo
    diverso restituisce `409 idempotency_conflict`. Leghi la chiave alla vendita e al tentativo, come in
    `order-1042-attempt-1`.

    ### Seguire il risultato [#track-the-result]

    Segua la richiesta attraverso i suoi [stati](#request-statuses) con i webhook, oppure interroghi periodicamente
    `GET /ecr/payment-requests/{requestId}`. Quando raggiunge `payment_created`, la risposta contiene
    il `paymentToken` e gli eventi `payment.updated` riportano l'avanzamento del pagamento stesso.

    Per ricevere i webhook, registri il Suo endpoint HTTPS con `POST /ecr/webhook-subscriptions`, oppure imposti
    l'URL di callback e i tipi di evento per l'esercente nella 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="Conservi il signing secret" type="warn">
      `signingSecret` viene restituito solo alla creazione della sottoscrizione. Lo conservi in modo sicuro; Le
      serve per verificare la firma dei webhook in arrivo.
    </Callout>

    Una sottoscrizione appartiene all'integratore e all'esercente, non a un token di accesso, quindi resta valida
    anche dopo il rinnovo del token. Risponda a ogni evento con un `2xx` dopo averlo accettato.

    <Callout title="Verifichi prima di chiudere la vendita" type="error">
      I webhook sono notifiche, non prove di pagamento, e un webhook può arrivare in ritardo o non arrivare affatto. Non
      chiuda mai una vendita basandosi solo su un webhook:

      * **Quando arriva un webhook,** confermi il risultato finale tramite l'API, ad esempio con
        [Recuperare i dettagli del pagamento](/platform-api/reporting#retrieve-payment-details).
      * **Quando non arriva alcun webhook** entro un timeout adatto al Suo flusso di checkout, e comunque non oltre
        l'`expiresAt` della richiesta, chiami `GET /ecr/payment-requests/{requestId}` per leggerne
        lo stato attuale, quindi confermi il pagamento tramite l'API come indicato sopra.
    </Callout>
  </div>
</section>

## Riferimento [#reference]

### Endpoint [#endpoints]

| Metodo   | Endpoint                                      | Descrizione                                                                                               |
| -------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `POST`   | `/ecr/auth`                                   | Scambia il Suo token API e un `merchantToken` con un token di accesso.                                    |
| `POST`   | `/ecr/auth/refresh`                           | Rinnova un token di accesso ancora valido.                                                                |
| `GET`    | `/ecr/devices`                                | Elenca i dispositivi dell'esercente. Filtro facoltativo `connected=true`.                                 |
| `GET`    | `/ecr/devices/{externalDeviceId}`             | Restituisce un dispositivo e la sua disponibilità attuale.                                                |
| `POST`   | `/ecr/payment-requests`                       | Salva una richiesta di pagamento e avvisa il dispositivo. Richiede `Idempotency-Key`.                     |
| `GET`    | `/ecr/payment-requests/{requestId}`           | Restituisce lo stato della richiesta, l'eventuale motivo del rifiuto e il `paymentToken`.                 |
| `POST`   | `/ecr/webhook-subscriptions`                  | Registra un URL HTTPS e i tipi di evento. Restituisce il signing secret (chiave di firma) una sola volta. |
| `GET`    | `/ecr/webhook-subscriptions`                  | Elenca le Sue sottoscrizioni per l'esercente, senza signing secret.                                       |
| `DELETE` | `/ecr/webhook-subscriptions/{subscriptionId}` | Rimuove una sottoscrizione.                                                                               |

### Disponibilità del dispositivo [#device-availability]

Entrambi gli endpoint dei dispositivi restituiscono `externalDeviceId`, `name`, `connected`, `availability`, `lastSeenAt` e
`updatedAt`.

| Valore        | Descrizione                                                                                  |
| ------------- | -------------------------------------------------------------------------------------------- |
| `ready`       | Connesso e in grado di ricevere e mostrare una richiesta.                                    |
| `busy`        | Sta elaborando un pagamento o ha una richiesta attiva.                                       |
| `unavailable` | Disconnesso, senza una sessione valida o non in grado di mostrare la schermata di pagamento. |

### Stati della richiesta [#request-statuses]

| Stato             | Descrizione                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| `pending`         | Salvata e in attesa che il dispositivo la accetti.                                               |
| `acknowledged`    | Il dispositivo l'ha ricevuta; il pagamento potrebbe non esistere ancora.                         |
| `payment_created` | Esistono un pagamento e il relativo `paymentToken`. Segua l'ulteriore avanzamento sul pagamento. |
| `rejected`        | Il dispositivo o Tapaya ha rifiutato la richiesta.                                               |

### Errori [#errors]

| Codice        | Motivo                  | Descrizione                                                   |
| ------------- | ----------------------- | ------------------------------------------------------------- |
| `403` / `404` | Oggetto non disponibile | Non dispone dell'autorizzazione, oppure l'oggetto non esiste. |
| `409`         | `device_busy`           | Il dispositivo ha già una richiesta o un pagamento attivo.    |
| `409`         | `device_unavailable`    | Il dispositivo è connesso ma non è pronto.                    |
| `409`         | `idempotency_conflict`  | La chiave è già stata usata con un corpo diverso.             |
| `503`         | `device_offline`        | Il dispositivo non è connesso.                                |

Gli errori di convalida dell'importo e della valuta seguono le stesse convenzioni del resto della
[Platform API](/platform-api).

### Eventi webhook [#webhook-events]

| Evento                        | Inviato quando                                                                            | Contenuto di `data`                                                                                                    |
| ----------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `device.connected`            | Un dispositivo apre una nuova connessione attiva.                                         | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                          |
| `device.disconnected`         | La connessione si chiude o il controllo di attività della connessione va in timeout.      | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                          |
| `device.availability_changed` | La disponibilità passa tra `ready`, `busy` e `unavailable`.                               | `externalDeviceId`, `availability`, `reason` facoltativo                                                               |
| `payment_request.updated`     | La richiesta viene salvata, ricevuta, rifiutata, scade o le viene associato un pagamento. | `requestId`, `externalDeviceId`, `status`, `paymentToken`, `reference`, `reason`, `expiresAt`                          |
| `payment.updated`             | Lo stato del pagamento cambia.                                                            | `paymentToken`, `requestId`, `externalDeviceId`, `status`, `reference`, `amount`, `currency`, `receiptUrl` facoltativo |

Ogni evento arriva come HTTP `POST` con la stessa struttura (envelope):

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