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

Cloud Accept API дає змогу вашому електронному касовому апарату (ECR) або бекенду POS запускати платіж на пристрої Tapaya
через хмару. Ваша система ніколи не взаємодіє з пристроєм напряму: вона надсилає запит на платіж до Tapaya, Tapaya
сповіщає спарений пристрій, і пристрій показує клієнту екран оплати. Інформація про перебіг надходить вам
у вигляді вебхуків або ви отримуєте її опитуванням (polling).

## Виберіть спосіб спарювання пристроїв [#choose-how-to-pair-devices]

Кожен пристрій, на який ви надсилаєте платежі, потребує `externalDeviceId`: назви, під якою його знає ваша система, наприклад
`till-01`. Призначити його можна одним із двох способів:

<Cards>
  <VisualCard title="З вашого застосунку" visual="<Layers />" href="#assign-from-your-app" description="Ваш застосунок призначає externalDeviceId через Accept SDK на пристрої." />

  <VisualCard title="QR-код із платформи" visual="<QrCode />" href="#scan-a-qr-code" description="Мерчант показує QR-код у Tapaya Platform і сканує його застосунком для мерчантів." />
</Cards>

## Перш ніж почати [#before-you-start]

1. **Отримайте свій Server Secret Token** на [сторінці API Keys](https://platform.tapaya.com/api-keys/). Нижче ви обміняєте
   його на токен доступу в межах мерчанта (див. [Автентифікація в Platform API](/platform-api#authentication)).
2. **Зареєструйте мерчанта** і збережіть його `merchantToken`. Див.
   [Реєстрація нового мерчанта](./backend_integration#register-a-new-merchant).
3. **Підготуйте кожен пристрій** із застосунком Tapaya, у якому виконано вхід від імені цього мерчанта, щоб його можна було спарити
   і він міг отримувати запити.

<Callout title="Один токен, один мерчант" type="info">
  Кожен виклик Cloud Accept API стосується рівно одного мерчанта. Щоб обслуговувати кількох мерчантів, отримуйте через обмін
  окремий токен доступу для кожного з них.
</Callout>

<section id="cloud-accept-steps">
  <div className="fd-steps [&_h3]:fd-step">
    ### Спарювання пристрою [#pair-a-device]

    Спарювання призначає пристрою Tapaya `externalDeviceId`, щоб ви могли знайти його у списку пристроїв
    і вибрати його як цільовий під час запуску платежу.

    #### Призначення з вашого застосунку [#assign-from-your-app]

    Ваш застосунок викликає на пристрої функцію Accept SDK, яка призначає йому `externalDeviceId`. Використовуйте
    той самий ідентифікатор у кожному виклику Cloud Accept API для цього пристрою.

    #### Сканування QR-коду [#scan-a-qr-code]

    Мерчант відкриває процес спарювання в Tapaya Platform, яка показує QR-код для
    `externalDeviceId`, що призначається. Мерчант сканує його застосунком Tapaya для мерчантів на цільовому
    пристрої, і застосунок завершує спарювання.

    ### Обмін на токен доступу [#exchange-an-access-token]

    Обміняйте свій Server Secret Token і `merchantToken` мерчанта на токен доступу за допомогою
    `POST /ecr/auth`. `merchantToken` надсилається лише тут; усі подальші виклики надсилають тільки
    `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"
    }
    ```

    | Код   | Опис                                          |
    | ----- | --------------------------------------------- |
    | `401` | API-токен недійсний або строк його дії минув. |
    | `403` | Доступ до мерчанта заборонено.                |

    **Оновлюйте токен до настання `expiresAt`.** Викличте `POST /ecr/auth/refresh` із поточним, ще дійсним
    `accessToken` у заголовку `Authorization`. Ендпоінт повертає те саме тіло з новим токеном; збережіть його
    і надалі використовуйте саме його.

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

    | Код   | Опис                                                                                                       |
    | ----- | ---------------------------------------------------------------------------------------------------------- |
    | `401` | Токен доступу недійсний, строк його дії минув або його відкликано. Отримайте новий через `POST /ecr/auth`. |
    | `403` | Ви більше не маєте доступу до мерчанта. Новий токен не видається.                                          |

    ### Пошук готового пристрою [#find-a-ready-device]

    Отримайте список пристроїв мерчанта за допомогою `GET /ecr/devices`. Додайте `?connected=true`, щоб виключити
    відключені пристрої, або отримайте один пристрій за допомогою `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"
    }
    ```

    Надсилайте платежі лише на пристрій, у якого `availability` має значення `ready`. Список є лише знімком стану:
    доступність перевіряється повторно під час надсилання, тож навіть пристрій у стані `ready` може
    відхилити запит. Див. [Доступність пристрою](#device-availability).

    ### Надсилання запиту на платіж [#submit-a-payment-request]

    Надішліть суму на пристрій за допомогою `POST /ecr/payment-requests`. Tapaya зберігає запит і
    сповіщає пристрій, який показує екран оплати.

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

    **Параметри:**

    | Поле               | Тип       | Обов'язкове | Опис                                                          |
    | ------------------ | --------- | ----------- | ------------------------------------------------------------- |
    | `externalDeviceId` | `string`  | ✓           | Спарений пристрій, який має показати платіж.                  |
    | `amount`           | `integer` | ✓           | Сума в найменшій одиниці валюти (`12500` дорівнює 125,00 Kč). |
    | `tipAmount`        | `integer` |             | Чайові в найменшій одиниці валюти.                            |
    | `currency`         | `string`  | ✓           | Код валюти ISO 4217.                                          |
    | `paymentToken`     | `string`  |             | Наявний платіж, до якого слід прив'язати запит.               |

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

    <Callout title="202 не є результатом платежу" type="warn">
      `202 Accepted` означає, що Tapaya зберегла запит. Це не означає, що пристрій його отримав
      або що клієнт заплатив. Усе, що завершиться збоєм пізніше, надійде вам як зміна статусу.
    </Callout>

    **Зробіть повторні спроби безпечними.** Заголовок `Idempotency-Key` є обов'язковим. Повторне використання ключа з тим самим тілом
    повертає початковий запит замість запуску другого платежу; повторне використання з іншим
    тілом повертає `409 idempotency_conflict`. Прив'яжіть ключ до продажу та спроби, як у
    `order-1042-attempt-1`.

    ### Відстеження результату [#track-the-result]

    Стежте за [статусами](#request-statuses) запиту за допомогою вебхуків або періодично опитуйте
    `GET /ecr/payment-requests/{requestId}`. Щойно запит досягне статусу `payment_created`, відповідь міститиме
    `paymentToken`, а про перебіг самого платежу повідомлятимуть події `payment.updated`.

    Щоб отримувати вебхуки, зареєструйте свій HTTPS-ендпоінт за допомогою `POST /ecr/webhook-subscriptions` або задайте
    для мерчанта callback URL і типи подій у 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="Збережіть signing secret" type="warn">
      `signingSecret` повертається лише під час створення підписки. Збережіть його в захищеному місці; він
      знадобиться вам для перевірки підпису вхідних вебхуків.
    </Callout>

    Підписка належить інтегратору й мерчанту, а не токену доступу, тому вона зберігається після
    оновлення токена. Відповідайте на кожну подію кодом `2xx`, щойно приймете її.

    <Callout title="Перевірте результат, перш ніж закривати продаж" type="error">
      Вебхуки є сповіщеннями, а не підтвердженням оплати, і вебхук може надійти із запізненням або не надійти зовсім. Ніколи
      не закривайте продаж лише на підставі вебхука:

      * **Коли вебхук надходить,** підтвердьте кінцевий результат через API, наприклад за допомогою запиту з розділу
        [Отримання даних платежу](/platform-api/reporting#retrieve-payment-details).
      * **Коли вебхук не надходить** протягом тайм-ауту, що відповідає вашому процесу оплати, і не пізніше
        `expiresAt` запиту, викличте `GET /ecr/payment-requests/{requestId}`, щоб дізнатися його
        поточний статус, а потім підтвердьте платіж через API, як описано вище.
    </Callout>
  </div>
</section>

## Довідка [#reference]

### Ендпоінти [#endpoints]

| Метод    | Ендпоінт                                      | Опис                                                                                   |
| -------- | --------------------------------------------- | -------------------------------------------------------------------------------------- |
| `POST`   | `/ecr/auth`                                   | Обмінює ваш API-токен і `merchantToken` на токен доступу.                              |
| `POST`   | `/ecr/auth/refresh`                           | Оновлює ще дійсний токен доступу.                                                      |
| `GET`    | `/ecr/devices`                                | Повертає список пристроїв мерчанта. Необов'язковий фільтр `connected=true`.            |
| `GET`    | `/ecr/devices/{externalDeviceId}`             | Повертає один пристрій і його поточну доступність.                                     |
| `POST`   | `/ecr/payment-requests`                       | Зберігає запит на платіж і сповіщає пристрій. Потребує `Idempotency-Key`.              |
| `GET`    | `/ecr/payment-requests/{requestId}`           | Повертає статус запиту, причину відхилення (якщо є) і `paymentToken`.                  |
| `POST`   | `/ecr/webhook-subscriptions`                  | Реєструє HTTPS URL і типи подій. Signing secret (ключ підпису) повертає лише один раз. |
| `GET`    | `/ecr/webhook-subscriptions`                  | Повертає список ваших підписок для мерчанта без signing secret.                        |
| `DELETE` | `/ecr/webhook-subscriptions/{subscriptionId}` | Видаляє підписку.                                                                      |

### Доступність пристрою [#device-availability]

Обидва ендпоінти для пристроїв повертають `externalDeviceId`, `name`, `connected`, `availability`, `lastSeenAt` і
`updatedAt`.

| Значення      | Опис                                                                 |
| ------------- | -------------------------------------------------------------------- |
| `ready`       | Підключений і здатний отримати та показати запит.                    |
| `busy`        | Обробляє платіж або має активний запит.                              |
| `unavailable` | Відключений, не має дійсної сесії або не може показати екран оплати. |

### Статуси запиту [#request-statuses]

| Статус            | Опис                                                                             |
| ----------------- | -------------------------------------------------------------------------------- |
| `pending`         | Збережений і очікує, поки пристрій його прийме.                                  |
| `acknowledged`    | Пристрій його отримав; платежу ще може не існувати.                              |
| `payment_created` | Платіж і його `paymentToken` існують. Подальший перебіг відстежуйте за платежем. |
| `rejected`        | Пристрій або Tapaya відхилили запит.                                             |

### Помилки [#errors]

| Код           | Причина                | Опис                                        |
| ------------- | ---------------------- | ------------------------------------------- |
| `403` / `404` | Об'єкт недоступний     | У вас немає дозволу або об'єкт не існує.    |
| `409`         | `device_busy`          | Пристрій уже має активний запит або платіж. |
| `409`         | `device_unavailable`   | Пристрій підключений, але не готовий.       |
| `409`         | `idempotency_conflict` | Ключ уже використано з іншим тілом.         |
| `503`         | `device_offline`       | Пристрій не підключений.                    |

Помилки валідації суми та валюти відповідають тим самим правилам, що й решта
[Platform API](/platform-api).

### Події вебхуків [#webhook-events]

| Подія                         | Надсилається, коли                                                                            | Вміст `data`                                                                                                             |
| ----------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `device.connected`            | Пристрій відкриває нове активне з'єднання.                                                    | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                            |
| `device.disconnected`         | З'єднання закривається або спливає тайм-аут перевірки активності.                             | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                            |
| `device.availability_changed` | Доступність змінюється між `ready`, `busy` і `unavailable`.                                   | `externalDeviceId`, `availability`, необов'язково `reason`                                                               |
| `payment_request.updated`     | Запит збережено, підтверджено, відхилено, строк його дії минув або для нього створено платіж. | `requestId`, `externalDeviceId`, `status`, `paymentToken`, `reference`, `reason`, `expiresAt`                            |
| `payment.updated`             | Змінюється статус платежу.                                                                    | `paymentToken`, `requestId`, `externalDeviceId`, `status`, `reference`, `amount`, `currency`, необов'язково `receiptUrl` |

Кожна подія надходить як HTTP `POST` в однаковій обгортці:

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