# Cloud Accept API (/vi/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 cho phép máy tính tiền điện tử (ECR) hoặc backend POS của bạn bắt đầu một khoản thanh toán trên thiết bị Tapaya
qua đám mây. Hệ thống của bạn không bao giờ giao tiếp trực tiếp với thiết bị: hệ thống gửi một yêu cầu thanh toán tới Tapaya, Tapaya
thông báo cho thiết bị đã ghép nối, và thiết bị hiển thị màn hình thanh toán cho khách hàng. Tiến trình được gửi lại cho bạn
dưới dạng webhook, hoặc bạn tự thăm dò (poll).

## Chọn cách ghép nối thiết bị [#choose-how-to-pair-devices]

Mỗi thiết bị mà bạn gửi thanh toán tới đều cần một `externalDeviceId`: tên mà hệ thống của bạn dùng cho thiết bị đó, chẳng hạn
`till-01`. Gán định danh này theo một trong hai cách:

<Cards>
  <VisualCard title="Từ ứng dụng của bạn" visual="<Layers />" href="#assign-from-your-app" description="Ứng dụng của bạn gán externalDeviceId thông qua Accept SDK trên thiết bị." />

  <VisualCard title="Mã QR từ nền tảng" visual="<QrCode />" href="#scan-a-qr-code" description="Merchant hiển thị một mã QR trong Tapaya Platform và quét mã đó bằng ứng dụng merchant." />
</Cards>

## Trước khi bắt đầu [#before-you-start]

1. **Lấy Server Secret Token của bạn** từ [trang API Keys](https://platform.tapaya.com/api-keys/). Bạn đổi
   token này lấy một access token theo phạm vi merchant ở bên dưới (xem [xác thực Platform API](/platform-api#authentication)).
2. **Đăng ký merchant** và lưu lại `merchantToken` của merchant đó. Xem
   [Đăng ký merchant mới](./backend_integration#register-a-new-merchant).
3. **Chuẩn bị từng thiết bị** với ứng dụng Tapaya đã đăng nhập cho merchant đó, để thiết bị có thể được ghép nối và nhận
   yêu cầu.

<Callout title="Một token, một merchant" type="info">
  Mỗi lệnh gọi Cloud Accept API chỉ áp dụng cho đúng một merchant. Để phục vụ nhiều merchant, hãy đổi lấy một access token
  riêng cho từng merchant.
</Callout>

<section id="cloud-accept-steps">
  <div className="fd-steps [&_h3]:fd-step">
    ### Ghép nối thiết bị [#pair-a-device]

    Việc ghép nối gán một `externalDeviceId` cho thiết bị Tapaya, để bạn có thể tìm thấy thiết bị khi liệt kê danh sách thiết bị
    và chỉ định thiết bị đó khi bắt đầu một khoản thanh toán.

    #### Gán từ ứng dụng của bạn [#assign-from-your-app]

    Ứng dụng của bạn gọi một hàm của Accept SDK trên thiết bị để gán `externalDeviceId` cho thiết bị. Hãy dùng
    cùng một định danh trong mọi lệnh gọi Cloud Accept API cho thiết bị đó.

    #### Quét mã QR [#scan-a-qr-code]

    Merchant mở quy trình ghép nối trong Tapaya Platform, nơi hiển thị một mã QR cho
    `externalDeviceId` cần gán. Merchant quét mã bằng ứng dụng merchant của Tapaya trên thiết bị
    đích, và ứng dụng sẽ hoàn tất việc ghép nối.

    ### Đổi lấy access token [#exchange-an-access-token]

    Đổi Server Secret Token của bạn và `merchantToken` của merchant lấy một access token bằng
    `POST /ecr/auth`. `merchantToken` chỉ được gửi ở đây; mọi lệnh gọi sau đó chỉ gửi
    `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"
    }
    ```

    | Mã    | Mô tả                                   |
    | ----- | --------------------------------------- |
    | `401` | API token không hợp lệ hoặc đã hết hạn. |
    | `403` | Không được phép truy cập merchant.      |

    **Gia hạn trước `expiresAt`.** Gọi `POST /ecr/auth/refresh` với `accessToken` hiện tại, vẫn còn hiệu lực,
    trong header `Authorization`. Endpoint này trả về cùng nội dung phản hồi với một token mới; hãy lưu token đó
    và dùng nó từ đó trở đi.

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

    | Mã    | Mô tả                                                                                              |
    | ----- | -------------------------------------------------------------------------------------------------- |
    | `401` | Access token không hợp lệ, đã hết hạn hoặc đã bị thu hồi. Đổi lấy token mới bằng `POST /ecr/auth`. |
    | `403` | Bạn không còn quyền truy cập merchant. Không có token mới nào được cấp.                            |

    ### Tìm thiết bị sẵn sàng [#find-a-ready-device]

    Liệt kê các thiết bị của merchant bằng `GET /ecr/devices`. Thêm `?connected=true` để loại bỏ
    các thiết bị đã ngắt kết nối, hoặc lấy một thiết bị bằng `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"
    }
    ```

    Chỉ gửi thanh toán tới thiết bị có `availability` là `ready`. Danh sách chỉ là bản lưu (snapshot) tại một thời điểm:
    tình trạng sẵn sàng được kiểm tra lại khi bạn gửi yêu cầu, nên một thiết bị `ready` vẫn có thể từ chối
    yêu cầu. Xem [Tình trạng sẵn sàng của thiết bị](#device-availability).

    ### Gửi yêu cầu thanh toán [#submit-a-payment-request]

    Gửi số tiền tới một thiết bị bằng `POST /ecr/payment-requests`. Tapaya lưu yêu cầu và
    thông báo cho thiết bị, thiết bị sẽ hiển thị màn hình thanh toán.

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

    **Tham số:**

    | Trường             | Kiểu      | Bắt buộc | Mô tả                                                                 |
    | ------------------ | --------- | -------- | --------------------------------------------------------------------- |
    | `externalDeviceId` | `string`  | ✓        | Thiết bị đã ghép nối sẽ hiển thị khoản thanh toán.                    |
    | `amount`           | `integer` | ✓        | Số tiền theo đơn vị nhỏ nhất của loại tiền tệ (`12500` là 125,00 Kč). |
    | `tipAmount`        | `integer` |          | Tiền tip theo đơn vị nhỏ nhất của loại tiền tệ.                       |
    | `currency`         | `string`  | ✓        | Mã tiền tệ ISO 4217.                                                  |
    | `paymentToken`     | `string`  |          | Một khoản thanh toán hiện có để gắn yêu cầu vào.                      |

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

    <Callout title="202 không phải là kết quả thanh toán" type="warn">
      `202 Accepted` nghĩa là Tapaya đã lưu yêu cầu. Điều đó không có nghĩa là thiết bị đã nhận được yêu cầu
      hay khách hàng đã thanh toán. Mọi lỗi xảy ra sau đó sẽ được gửi tới bạn dưới dạng thay đổi trạng thái.
    </Callout>

    **Đảm bảo thử lại an toàn.** Header `Idempotency-Key` là bắt buộc. Dùng lại một key với cùng nội dung yêu cầu
    sẽ trả về yêu cầu ban đầu thay vì bắt đầu khoản thanh toán thứ hai; dùng lại key đó với nội dung
    khác sẽ trả về `409 idempotency_conflict`. Hãy gắn key với giao dịch bán hàng và lần thử, như trong
    `order-1042-attempt-1`.

    ### Theo dõi kết quả [#track-the-result]

    Theo dõi yêu cầu qua các [trạng thái](#request-statuses) của nó bằng webhook, hoặc thăm dò
    `GET /ecr/payment-requests/{requestId}`. Khi yêu cầu đạt `payment_created`, phản hồi sẽ chứa
    `paymentToken`, và các sự kiện `payment.updated` báo tiến trình của chính khoản thanh toán.

    Để nhận webhook, hãy đăng ký endpoint HTTPS của bạn bằng `POST /ecr/webhook-subscriptions`, hoặc đặt
    URL callback và các loại sự kiện cho merchant trong 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="Lưu signing secret" type="warn">
      `signingSecret` chỉ được trả về khi tạo đăng ký (subscription). Hãy lưu trữ nó an toàn; bạn
      cần nó để xác minh chữ ký của các webhook gửi đến.
    </Callout>

    Một đăng ký thuộc về bên tích hợp và merchant, không thuộc về access token, nên nó vẫn được giữ nguyên khi
    gia hạn token. Hãy phản hồi mỗi sự kiện bằng mã `2xx` khi bạn đã chấp nhận sự kiện đó.

    <Callout title="Xác minh trước khi chốt giao dịch bán hàng" type="error">
      Webhook là thông báo, không phải bằng chứng thanh toán, và một webhook có thể đến muộn hoặc hoàn toàn không đến. Không bao giờ
      chốt giao dịch bán hàng chỉ dựa vào webhook:

      * **Khi có webhook đến,** hãy xác nhận kết quả cuối cùng qua API, ví dụ bằng
        [Truy xuất chi tiết thanh toán](/platform-api/reporting#retrieve-payment-details).
      * **Khi không có webhook nào đến** trong khoảng thời gian chờ phù hợp với quy trình thanh toán của bạn, và không muộn
        hơn `expiresAt` của yêu cầu, hãy gọi `GET /ecr/payment-requests/{requestId}` để đọc
        trạng thái hiện tại, sau đó xác nhận khoản thanh toán qua API như trên.
    </Callout>
  </div>
</section>

## Tham chiếu [#reference]

### Các endpoint [#endpoints]

| Phương thức | Endpoint                                      | Mô tả                                                                                       |
| ----------- | --------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `POST`      | `/ecr/auth`                                   | Đổi API token của bạn và một `merchantToken` lấy access token.                              |
| `POST`      | `/ecr/auth/refresh`                           | Gia hạn một access token vẫn còn hiệu lực.                                                  |
| `GET`       | `/ecr/devices`                                | Liệt kê các thiết bị của merchant. Bộ lọc `connected=true` tùy chọn.                        |
| `GET`       | `/ecr/devices/{externalDeviceId}`             | Trả về một thiết bị và tình trạng sẵn sàng hiện tại của thiết bị đó.                        |
| `POST`      | `/ecr/payment-requests`                       | Lưu một yêu cầu thanh toán và thông báo cho thiết bị. Bắt buộc có header `Idempotency-Key`. |
| `GET`       | `/ecr/payment-requests/{requestId}`           | Trả về trạng thái yêu cầu, lý do từ chối (nếu có) và `paymentToken`.                        |
| `POST`      | `/ecr/webhook-subscriptions`                  | Đăng ký một URL HTTPS và các loại sự kiện. Trả về signing secret một lần duy nhất.          |
| `GET`       | `/ecr/webhook-subscriptions`                  | Liệt kê các đăng ký của bạn cho merchant, không kèm signing secret.                         |
| `DELETE`    | `/ecr/webhook-subscriptions/{subscriptionId}` | Xóa một đăng ký.                                                                            |

### Tình trạng sẵn sàng của thiết bị [#device-availability]

Cả hai endpoint thiết bị đều trả về `externalDeviceId`, `name`, `connected`, `availability`, `lastSeenAt` và
`updatedAt`.

| Giá trị       | Mô tả                                                                               |
| ------------- | ----------------------------------------------------------------------------------- |
| `ready`       | Đã kết nối, có thể nhận và hiển thị yêu cầu.                                        |
| `busy`        | Đang xử lý một khoản thanh toán hoặc đang giữ một yêu cầu đang hoạt động.           |
| `unavailable` | Đã ngắt kết nối, không có phiên hợp lệ hoặc không thể hiển thị màn hình thanh toán. |

### Trạng thái yêu cầu [#request-statuses]

| Trạng thái        | Mô tả                                                                                                 |
| ----------------- | ----------------------------------------------------------------------------------------------------- |
| `pending`         | Đã được lưu và đang chờ thiết bị chấp nhận.                                                           |
| `acknowledged`    | Thiết bị đã nhận được yêu cầu; khoản thanh toán có thể chưa tồn tại.                                  |
| `payment_created` | Đã có khoản thanh toán và `paymentToken` của nó. Theo dõi tiến trình tiếp theo trên khoản thanh toán. |
| `rejected`        | Thiết bị hoặc Tapaya đã từ chối yêu cầu.                                                              |

### Lỗi [#errors]

| Mã            | Lý do                    | Mô tả                                                            |
| ------------- | ------------------------ | ---------------------------------------------------------------- |
| `403` / `404` | Đối tượng không khả dụng | Bạn không có quyền, hoặc đối tượng không tồn tại.                |
| `409`         | `device_busy`            | Thiết bị đã có một yêu cầu hoặc khoản thanh toán đang hoạt động. |
| `409`         | `device_unavailable`     | Thiết bị đã kết nối nhưng chưa sẵn sàng.                         |
| `409`         | `idempotency_conflict`   | Key đã được dùng với một nội dung yêu cầu khác.                  |
| `503`         | `device_offline`         | Thiết bị không có kết nối.                                       |

Lỗi kiểm tra tính hợp lệ của số tiền và loại tiền tệ tuân theo cùng quy ước như phần còn lại của
[Platform API](/platform-api).

### Sự kiện webhook [#webhook-events]

| Sự kiện                       | Được gửi khi                                                                           | Nội dung `data`                                                                                                     |
| ----------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `device.connected`            | Một thiết bị mở kết nối trực tiếp mới.                                                 | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                       |
| `device.disconnected`         | Kết nối bị đóng hoặc việc kiểm tra hoạt động (liveness check) hết thời gian chờ.       | `externalDeviceId`, `connected`, `availability`, `lastSeenAt`                                                       |
| `device.availability_changed` | Tình trạng sẵn sàng chuyển giữa `ready`, `busy` và `unavailable`.                      | `externalDeviceId`, `availability`, `reason` tùy chọn                                                               |
| `payment_request.updated`     | Yêu cầu được lưu, được xác nhận đã nhận, bị từ chối, hết hạn hoặc có khoản thanh toán. | `requestId`, `externalDeviceId`, `status`, `paymentToken`, `reference`, `reason`, `expiresAt`                       |
| `payment.updated`             | Trạng thái của khoản thanh toán thay đổi.                                              | `paymentToken`, `requestId`, `externalDeviceId`, `status`, `reference`, `amount`, `currency`, `receiptUrl` tùy chọn |

Mọi sự kiện đều đến dưới dạng HTTP `POST` trong cùng một cấu trúc bao (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"
    }
}
```
