TAŇBA-OOH Разработчикам

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

ИмяТипОбязательныйОписаниеПример
seqintegerдаНомер heartbeat; он должен быть больше последнего принятого.1840
statestringдаСостояние плеера. normal, compliance, cutoff
noncestringдаЗначение nonce или next_nonce из последнего ответа."3b9f0c6e2a7d41f85e0b9a3c7d2e1f60"
firmwarestringВерсия прошивки."4.2.1"
spec_versionstringВерсия спецификации соответствия."1.1"
log_head_hashstringSHA-256 в hex последней строки локального журнала с цепочкой хэшей."9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123"
tsstring (date-time)Часы плеера."2026-10-14T09:05:00Z"
current_approvalstringНомер согласования на экране сейчас."TNB-2026-000123"
revocation_list_versionintegerВерсия используемого списка отзыва.42
edid_sha256stringSHA-256 блока EDID дисплея в hex."4d7a1c0e9b3f58a26c4e1d07b9a3f5c8e2d60b14a7c93e5f8d1b2a6c0e4f7391"
screen_width_pxintegerШирина дисплея в пикселях, как ее видит плеер. Передавайте вместе с screen_height_px или не передавайте; пара, которая 24 часа отличается от размера, записанного для конструкции, фиксируется как несовпадение размера экрана.1920
screen_height_pxintegerВысота дисплея в пикселях, как ее видит плеер; см. screen_width_px.1080
qr_keybooleanFalse, пока у плеера нет ключа присутствия, 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_timestring (date-time)даЧасы платформы."2026-10-14T09:05:00Z"
next_noncestringдаОдноразовое число для следующего heartbeat."c4e7a19b0d2f63858a1e9c7b3d05f246"
commandsarray of stringдаПодписанные конверты команд для этого плеера, base64 от DER; не больше нескольких старейших, еще не подтвержденных.
revocation_listobject | nullНовейший список отзыва: его версия и адрес.
revocation_list.versionintegerВерсия списка.42
revocation_list.urlstringАдрес списка."https://devices.tanba-ooh.kz/d/v1/revocations/42"
telemetry_cut_sintegerдаЧерез сколько секунд плеер закрывает пакет телеметрии; он закрывается и при 1000 событий.300
qr_secretstring64 символа 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))
}