Heartbeat устройства (клиентский сертификат, подписанное тело)
POST /d/v1/heartbeat
https://devices.tanba-ooh.kz/d/v1/heartbeat
Плеер сообщает свое состояние и получает команды. Тело подписано ключом устройства (заголовок X-Tanba-Signature, ES256, r||s, base64url), и в нем есть возрастающий seq и nonce, выданный в прошлом ответе. Если указан wait (до 25 секунд), платформа держит ответ, пока не появится команда: так отключение и восстановление доходят до подключенного плеера за секунды. Плеер, который делает токены присутствия для QR-кода, шлет qr_key: false, пока ключа нет, и true после; пока значение false, ответ несет qr_secret и возвращается сразу.
Расхождение часов больше пяти минут записывается как сдвиг часов. Изменение edid_sha256 записывается как смена экрана. Размер дисплея (screen_width_px и screen_height_px), который в каждом heartbeat на протяжении 24 часов отличается от записанного для конструкции, фиксируется как несовпадение размера экрана; heartbeat без размера ничего не меняет. Смена сетевого префикса вызывающего (/24 для IPv4, /48 для IPv6) как смена сети. Молчание дольше льготного срока класса (10 минут для классов A и B, 60 для C) сохраняется как интервал отсутствия связи, когда плеер снова отвечает.
Аутентификация
Клиентский сертификат устройства (взаимный TLS) и подпись тела ключом устройства.
Параметры
| Имя | Где | Тип | Обязательный | Описание | Пример |
|---|---|---|---|---|---|
X-Tanba-Signature | заголовок | string | да | Подпись ES256 точных байтов тела ключом устройства, сырые r||s (64 байта), base64url без заполнения. | q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw |
wait | запрос | integer | нет | Сколько секунд держать ответ до появления команды; от 0 до 25. | 25 |
Тело запроса
application/json
| Имя | Тип | Обязательный | Описание | Пример |
|---|---|---|---|---|
seq | integer | да | Номер heartbeat; он должен быть больше последнего принятого. | 1840 |
state | string | да | Состояние плеера. normal, compliance, cutoff | |
nonce | string | да | Значение nonce или next_nonce из последнего ответа. | "3b9f0c6e2a7d41f85e0b9a3c7d2e1f60" |
firmware | string | Версия прошивки. | "4.2.1" | |
spec_version | string | Версия спецификации соответствия. | "1.1" | |
log_head_hash | string | SHA-256 в hex последней строки локального журнала с цепочкой хэшей. | "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123" | |
ts | string (date-time) | Часы плеера. | "2026-10-14T09:05:00Z" | |
current_approval | string | Номер согласования на экране сейчас. | "TNB-2026-000123" | |
revocation_list_version | integer | Версия используемого списка отзыва. | 42 | |
edid_sha256 | string | SHA-256 блока EDID дисплея в hex. | "4d7a1c0e9b3f58a26c4e1d07b9a3f5c8e2d60b14a7c93e5f8d1b2a6c0e4f7391" | |
screen_width_px | integer | Ширина дисплея в пикселях, как ее видит плеер. Передавайте вместе с screen_height_px или не передавайте; пара, которая 24 часа отличается от размера, записанного для конструкции, фиксируется как несовпадение размера экрана. | 1920 | |
screen_height_px | integer | Высота дисплея в пикселях, как ее видит плеер; см. screen_width_px. | 1080 | |
qr_key | boolean | False, пока у плеера нет ключа присутствия, true после. | true |
Пример
{
"seq": 1840,
"state": "normal",
"nonce": "3b9f0c6e2a7d41f85e0b9a3c7d2e1f60",
"firmware": "4.2.1",
"spec_version": "1.1",
"ts": "2026-10-14T09:05:00Z",
"current_approval": "TNB-2026-000123",
"revocation_list_version": 42,
"qr_key": true
}
Ответы
200 Принято.
application/json
| Имя | Тип | Обязательный | Описание | Пример |
|---|---|---|---|---|
server_time | string (date-time) | да | Часы платформы. | "2026-10-14T09:05:00Z" |
next_nonce | string | да | Одноразовое число для следующего heartbeat. | "c4e7a19b0d2f63858a1e9c7b3d05f246" |
commands | array of string | да | Подписанные конверты команд для этого плеера, base64 от DER; не больше нескольких старейших, еще не подтвержденных. | |
revocation_list | object | null | Новейший список отзыва: его версия и адрес. | ||
revocation_list.version | integer | Версия списка. | 42 | |
revocation_list.url | string | Адрес списка. | "https://devices.tanba-ooh.kz/d/v1/revocations/42" | |
telemetry_cut_s | integer | да | Через сколько секунд плеер закрывает пакет телеметрии; он закрывается и при 1000 событий. | 300 |
qr_secret | string | 64 символа hex; есть только пока плеер сообщает qr_key false. | "7b1d4e90a2c65f38e0b7a91d3c5f8264b0e9d1a7c3f5826e4b0d9a1c7e3f5824" |
Пример
{
"server_time": "2026-10-14T09:05:00Z",
"next_nonce": "c4e7a19b0d2f63858a1e9c7b3d05f246",
"commands": [
"MIIC0QYJKoZIhvcNAQcCoIICwjCCAr4CAQExDzANBglghkgBZQMEAgEFADA"
],
"revocation_list": {
"version": 42,
"url": "https://devices.tanba-ooh.kz/d/v1/revocations/42"
},
"telemetry_cut_s": 300,
"qr_secret": "7b1d4e90a2c65f38e0b7a91d3c5f8264b0e9d1a7c3f5826e4b0d9a1c7e3f5824"
}
Ошибки
Ошибки приходят документом problem (application/problem+json) со стабильным полем code.
| Статус | Значение |
|---|---|
| 401 | Подпись не проходит (bad_signature) либо устройство неизвестно, отозвано или просрочено (device_unauthorized). |
| 409 | Значение seq не больше прошлого (replay) либо nonce не тот, что выдан (nonce_mismatch; в ответе есть next_nonce). |
| 422 | Ошибки полей (validation_failed). |
| default | Ошибка в виде документа problem. |
Примеры кода
curl
curl -X POST 'https://devices.tanba-ooh.kz/d/v1/heartbeat' \
-H 'X-Tanba-Signature: q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw' \
-H 'Content-Type: application/json' \
-d '{
"seq": 1840,
"state": "normal",
"nonce": "3b9f0c6e2a7d41f85e0b9a3c7d2e1f60",
"firmware": "4.2.1",
"spec_version": "1.1",
"ts": "2026-10-14T09:05:00Z",
"current_approval": "TNB-2026-000123",
"revocation_list_version": 42,
"qr_key": true
}'
Python
import requests
response = requests.post(
"https://devices.tanba-ooh.kz/d/v1/heartbeat",
headers={
"X-Tanba-Signature": "q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw",
"Content-Type": "application/json",
},
json={
"seq": 1840,
"state": "normal",
"nonce": "3b9f0c6e2a7d41f85e0b9a3c7d2e1f60",
"firmware": "4.2.1",
"spec_version": "1.1",
"ts": "2026-10-14T09:05:00Z",
"current_approval": "TNB-2026-000123",
"revocation_list_version": 42,
"qr_key": True,
},
)
print(response.status_code, response.text)
Go
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{
"seq": 1840,
"state": "normal",
"nonce": "3b9f0c6e2a7d41f85e0b9a3c7d2e1f60",
"firmware": "4.2.1",
"spec_version": "1.1",
"ts": "2026-10-14T09:05:00Z",
"current_approval": "TNB-2026-000123",
"revocation_list_version": 42,
"qr_key": true
}`)
req, err := http.NewRequest("POST", "https://devices.tanba-ooh.kz/d/v1/heartbeat", body)
if err != nil {
panic(err)
}
req.Header.Set("X-Tanba-Signature", "q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
fmt.Println(resp.Status, string(data))
}