Передача показов с плееров и из CMS
Показ, который никто не может проверить, остается лишь заявлением. В этом руководстве показано, как плеер или CMS, которая им управляет, сообщает о завершенных показах в формате tanba.pop/1, чтобы платформа могла их посчитать, а любой желающий мог сверить счета с сохраненными пакетами.
Страницы справочника: API учета показов и Протокол устройства.
Выберите канал
Канал, по которому пришел пакет, определяет, насколько платформа ему доверяет. Тело пакета уровень не заявляет.
| Канал | Путь и хост | Аутентификация | Уровень доказательств |
|---|---|---|---|
| Сертификат | POST /d/v1/telemetry на https://devices.tanba-ooh.kz | Клиентский сертификат и подпись тела ключом устройства | mtls |
| Подпись | POST /s/v1/telemetry на https://agents.tanba-ooh.kz | Подпись тела ключом, записанным при регистрации; в теле есть device_id и ts | signed |
| CMS (подтверждены оператором) | POST /api/v1/pop/batches на https://app.tanba-ooh.kz | Ключ API с правом write:plays и подпись ключом, записанным для установки CMS | attested |
Какие уровни засчитываются в сбор, задает платформа, а не этот интерфейс. Показы, подтвержденные оператором, это собственное утверждение оператора: они никогда не снимают с оператора нарушение и служат перекрестной проверкой рядом с тем, о чем сообщают устройства.
Пакет
{"schema": "tanba.pop/1", "batch_seq": 1042, "prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"sent_at": "2026-10-14T09:05:00.120Z", "events": [ ]}
batch_seqэто целое число от 0, занимается один раз для пакета. Платформа хранит тело ровно так, как получила.prev_sha256это SHA-256 в hex тела пакетаbatch_seq - 1; для первого пакета пусто.- Не больше 1000 событий и 4 МБ в пакете (1 МБ на подписанном канале). Закрывайте пакет каждые 300 секунд или по достижении 1000 событий, смотря что наступит раньше. Ответ на heartbeat несет
telemetry_cut_s, интервал, который просит платформа. У пакета CMS вместоeventsстоитrecords, пределы ниже. - Отправляйте пакеты по порядку и останавливайтесь на первой ошибке. Неудавшийся пакет отправляйте повторно теми же байтами.
- Тело без
schemaэто прежний формат. После того как первый пакетtanba.pop/1устройства принят, вернуться назад нельзя: пакет безschemaотклоняется как409 legacy_after_pop. Все, что накопилось в прежнем формате, отправьте до первого пакетаtanba.pop/1.
Записи
Одна запись play на каждый завершенный показ:
| Поле | Тип | Правило |
|---|---|---|
kind | "play" | Другие виды: reject (с полем code, одним из NO_SIGNATURE, BAD_SIGNATURE, HASH_MISMATCH, EXPIRED_APPROVAL, REVOKED), mode_change и tamper; у них есть at |
seq | целое число | Обязательно для play. Для каждого плеера +1 на каждую запись через границы пакетов, с 1 |
play_id | строка, от 8 до 64 символов A-Za-z0-9_- | Уникален для плеера |
approval_number | TNB-YYYY-NNNNNN | Обязателен, если не задан slot |
slot | gov или emergency | Материал уполномоченного органа: без номера согласования, никогда не тарифицируется, считается отдельно |
media_sha256 | 64 символа hex | SHA-256 показанного файла |
start, end | RFC 3339, миллисекунды | start не позже end |
duration_ms | целое число от 0 | Измеренное время на экране |
structure | код конструкции | Необязательно; должен быть кодом самого плеера, иначе запись получит флаг |
screen | {"power": ...} | Необязательно; состояние дисплея во время показа: on, off или unknown |
Как считаются показы
- Показ это принятая запись с
duration_msне меньше 1000, которая засчитывается в час UTC поend. Более короткие записи принимаются и нигде не считаются. - Показ полный, если он длился с точностью до 500 мс до длины согласованного материала (для неподвижного изображения 1000 мс).
- Запись, которая принята, но имеет флаг, все равно считается. Флаги:
approval_unknown,approval_not_for_structure,approval_outside_validity,revoked_at_time,media_mismatch,structure_mismatch,duration_mismatch,clock_skew,late,screen_off,incompleteиlegacy. - Запись отклоняется, не считается и перечисляется в ответе с одной из причин:
kind_unknown,approval_number_invalid,time_invalid,end_before_start,time_in_future,too_old,seq_invalid,media_sha256_invalid. На канале CMS есть еще одна:player_unknown. - Показ с
screen.powerравнымoffзасчитывается и отмечается какscreen_off. Флаг никогда не убирает показ из счета.
Порядок, повторы и цепочка
- Тот же
batch_seqс теми же байтами получает ответ200сduplicate: trueи считается один раз. Тот жеbatch_seqс другими байтами это409 batch_conflict, и ничего не считается. - Для каждого плеера платформа хранит наибольший принятый номер записи. Запись с номером не выше него это дубль: перечисляется, но не считается. Повторная отправка старых записей под новым
batch_seqникогда не удваивает счет; платформа записывает это как откат нумерации. - Номер записи более чем на единицу выше наибольшего принятого принимается и записывается как пропуск. Плеер, потерявший нумерацию, регистрируется заново как новое устройство.
- Если у платформы есть пакет
batch_seq - 1и его хэш отличается отprev_sha256, пакет все равно принимается, а разрыв цепочки записывается.
Ответ
{"batch_seq": 1042, "received": 12, "accepted": 10, "duplicates": 0, "refused": 2, "flagged": 1, "duplicate": false,
"items": [{"index": 1, "status": "flagged", "flags": ["incomplete"]}, {"index": 10, "status": "refused", "reason": "time_in_future"},
{"index": 11, "status": "refused", "reason": "end_before_start"}]}
В items перечислены только записи, принятые не безоговорочно. index считается с 0. У чистого пакета список пуст.
Передача из CMS
Оператор, чьи плееры работают под CMS, может сообщать об их показах из CMS одним подписанным пакетом на многих плееров.
Разовая настройка в консоли
Это может сделать только администратор организации оператора, в разделе «Интеграции». Каждый шаг пишется в журнал аудита.
- Зарегистрируйте установку CMS: имя, тип и открытый ключ, ключ ECDSA P-256 в виде base64 от DER SubjectPublicKeyInfo. Закрытый ключ никогда не покидает вашу CMS. Консоль покажет идентификатор источника, который входит в каждый пакет.
- Сопоставьте каждый плеер CMS, который показывает на одной из ваших конструкций: идентификатор, под которым его знает CMS (
playerв записях, до 64 символов), и конструкция. Внешний идентификатор используется один раз для установки. Показы плеера без сопоставления отклоняются какplayer_unknown. - Создайте ключ API с правом
write:plays. См. Аутентификация и ключи API. - Чтобы заменить ключ, зарегистрируйте новый открытый ключ для установки. Новый ключ действует сразу; старый остается действительным еще 24 часа, затем отклоняется ответом
401 source_unauthorized. - Если ключ утек, завершите его сразу:
POST /api/v1/pop-sources/{id}/keys/{fingerprint}/revoke(администратор оператора). Источник не принимает пакеты, пока не зарегистрирован новый ключ. Тело каждого пакета должно содержатьsource_id, совпадающий с заголовкомX-Tanba-Source.
Подпись пакета
Подпись это ES256 над точными байтами тела JSON, после распаковки, если вы отправляете gzip. Она записывается сырой: r, затем s, по 32 байта, в base64url без заполнения. Сохраните помощник ниже как pop_client.py. Ему нужны Python 3 и пакет cryptography.
import base64, gzip, hashlib, json, sys, urllib.request
from datetime import datetime, timezone
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
def new_key(path):
"""Run once. Keep the private key; register the printed public key in the console."""
key = ec.generate_private_key(ec.SECP256R1())
with open(path, "wb") as f:
f.write(key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))
der = key.public_key().public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)
print("public key:", base64.b64encode(der).decode())
def load_key(path):
with open(path, "rb") as f:
return serialization.load_pem_private_key(f.read(), password=None)
def sign(key, body: bytes) -> str:
r, s = utils.decode_dss_signature(key.sign(body, ec.ECDSA(hashes.SHA256())))
return base64.urlsafe_b64encode(r.to_bytes(32, "big") + s.to_bytes(32, "big")).rstrip(b"=").decode()
def send(base, api_key, source_id, key, batch_seq, prev_sha256, records):
body = json.dumps({"schema": "tanba.pop/1", "source_id": source_id, "batch_seq": batch_seq, "prev_sha256": prev_sha256,
"sent_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"), "records": records},
separators=(",", ":")).encode()
req = urllib.request.Request(base + "/api/v1/pop/batches", data=gzip.compress(body), method="POST", headers={
"Authorization": "Bearer " + api_key, "X-Tanba-Source": source_id, "X-Tanba-Signature": sign(key, body),
"Content-Type": "application/json", "Content-Encoding": "gzip"})
with urllib.request.urlopen(req, timeout=60) as resp:
answer = json.load(resp)
return answer, hashlib.sha256(body).hexdigest() # the hash is the next batch's prev_sha256
if __name__ == "__main__":
if sys.argv[1] == "keygen": # python3 pop_client.py keygen cms-key.pem
new_key(sys.argv[2])
elif sys.argv[1] == "sign": # python3 pop_client.py sign cms-key.pem batch.json
print(sign(load_key(sys.argv[2]), open(sys.argv[3], "rb").read()))
Создайте ключ один раз и зарегистрируйте напечатанный открытый ключ в консоли:
python3 pop_client.py keygen cms-key.pem
Отправка через curl
Запишите пакет в batch.json, подпишите файл и отправьте те же байты. Используйте --data-binary: -d убрал бы переводы строк, и подпись перестала бы совпадать.
export POP_API_KEY='tnbk_EXAMPLE_secret'
export POP_SOURCE='3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836'
cat > batch.json <<'EOF'
{"schema":"tanba.pop/1","source_id":"3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836","batch_seq":1042,"prev_sha256":"","sent_at":"2026-10-14T09:05:00.120Z","records":[{"kind":"play","player":"1001:55:2","seq":18,"play_id":"7c1e9a40b2d35f68","approval_number":"TNB-2026-000123","media_sha256":"9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123","start":"2026-10-14T09:04:50.000Z","end":"2026-10-14T09:05:00.000Z","duration_ms":10000}]}
EOF
curl -X POST 'https://app.tanba-ooh.kz/api/v1/pop/batches' \
-H "Authorization: Bearer $POP_API_KEY" \
-H "X-Tanba-Source: $POP_SOURCE" \
-H "X-Tanba-Signature: $(python3 pop_client.py sign cms-key.pem batch.json)" \
-H 'Content-Type: application/json' \
--data-binary @batch.json
Отправка из Python
from pop_client import load_key, send
key = load_key("cms-key.pem")
records = [{"kind": "play", "player": "1001:55:2", "seq": 18, "play_id": "7c1e9a40b2d35f68",
"approval_number": "TNB-2026-000123",
"media_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"start": "2026-10-14T09:04:50.000Z", "end": "2026-10-14T09:05:00.000Z", "duration_ms": 10000}]
answer, body_hash = send("https://app.tanba-ooh.kz", "tnbk_EXAMPLE_secret", "3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836",
key, batch_seq=1042, prev_sha256="", records=records)
print(answer)
Сохраняйте body_hash как prev_sha256 следующего пакета. Счетчики seq плееров, batch_seq и хэш тела храните в собственном хранилище, чтобы после перезапуска нумерация продолжалась, а не повторялась.
Как привести вашу выгрузку к записям
player: внешний идентификатор, под которым вы сопоставили плеер, например три идентификатора кадра через:.end: в UTC. Если в выгрузке местное время площадки, переведите его; пояс для Казахстана Asia/Almaty.start:endминус длительность, если в выгрузке есть только длительность.play_id: любой устойчивый идентификатор показа, уникальный для плеера, например первые 16 hex-символов SHA-256 строки выгрузки.seq: присваивает ваш адаптер, +1 на каждую запись плеера в порядкеend.approval_numberиmedia_sha256: из таблицы, которую вы ведете и в которой каждый ролик связан с согласованием, по которому он идет, и хэшем его файла. Строку, чьего ролика нет в таблице, передать нельзя; не отправляйте ее и покажите оператору.
Пределы
| Записей в пакете | 10 000 |
| Тело | 4 МБ в пути; 16 МБ после распаковки и не больше чем в 20 раз больше сжатого размера |
| Пакеты | 2 в секунду на установку (всплеск 10) |
| Вызовы | 10 в секунду на ключ API (всплеск 30) |
| Записи | 2 000 000 в час на организацию |
Запрос сверх предела получает 429 rate_limited с Retry-After, а при превышении размера 413.
Передача с плеера
Плеер один раз регистрируется по токену из консоли, затем сообщает о себе сам. На канале сертификата он предъявляет клиентский сертификат и подписывает тело ключом устройства; на подписанном канале он подписывает тело ключом, записанным при регистрации, а в теле есть device_id и ts, которое должно быть не дальше 300 секунд от часов платформы (оба канала принимают один пакет в 10 секунд на устройство, до 3 подряд после паузы). Подпись делается точно так же, как выше. Вызовы перечислены на странице Протокол устройства.
Ошибки
Ошибки приходят документами problem со стабильным полем code.
| Статус | code | Когда |
|---|---|---|
| 400 | invalid_json, schema_unknown | Тело не читается, неизвестная schema |
| 400 / 401 | bad_signature | Подпись не проходит (400 на канале сертификата, где тело хранится отдельно как свидетельство; на остальных 401, ничего не сохраняется) |
| 401 | device_unauthorized, device_cert_required, stale_request | Учетные данные неизвестны, отозваны или просрочены; ts вне диапазона на подписанном канале |
| 401 | source_unauthorized | Канал CMS: источник неизвестен или выключен, нет заголовка X-Tanba-Source или ключ заменен больше 24 часов назад |
| 403 | forbidden, key_scope_denied | Канал CMS: источник принадлежит другой организации, вызывает не ключ API или у ключа нет write:plays |
| 409 | batch_conflict | batch_seq занят пакетом с другим содержимым |
| 409 | legacy_after_pop | Пакет в прежнем формате от устройства, которое уже присылало tanba.pop/1; не повторяйте его |
| 413 | body_too_large, batch_too_large | Больше пределов размера |
| 415 | unsupported_media_type | Канал CMS: не JSON или кодирование, отличное от gzip |
| 422 | validation_failed | Поля оболочки: batch_seq, формат prev_sha256, пустой events, отсутствующий или не совпадающий source_id |
| 429 | rate_limited | Превышены пределы частоты |
Проверьте, что сохранено
Каждый принятый пакет хранится точными полученными байтами вместе с заголовком подписи, SHA-256 тела и учетными данными, которыми он подписан. Любой, у кого есть открытый ключ, может взять сохраненный пакет, посчитать его хэш, проверить подпись и пройти цепочку по prev_sha256 и номера записей по seq, не доверяя счетам платформы. Как платформа каждый день запечатывает полученную статистику и как проверить печать офлайн, описано в руководстве Как читать выписку и проверять ее доказательства. Программа проверки лежит на странице Загрузки.