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

Передача показов с плееров и из 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 и tssigned
CMS (подтверждены оператором)POST /api/v1/pop/batches на https://app.tanba-ooh.kzКлюч API с правом write:plays и подпись ключом, записанным для установки CMSattested

Какие уровни засчитываются в сбор, задает платформа, а не этот интерфейс. Показы, подтвержденные оператором, это собственное утверждение оператора: они никогда не снимают с оператора нарушение и служат перекрестной проверкой рядом с тем, о чем сообщают устройства.

Пакет

{"schema": "tanba.pop/1", "batch_seq": 1042, "prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
 "sent_at": "2026-10-14T09:05:00.120Z", "events": [ ]}

Записи

Одна запись 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_numberTNB-YYYY-NNNNNNОбязателен, если не задан slot
slotgov или emergencyМатериал уполномоченного органа: без номера согласования, никогда не тарифицируется, считается отдельно
media_sha25664 символа hexSHA-256 показанного файла
start, endRFC 3339, миллисекундыstart не позже end
duration_msцелое число от 0Измеренное время на экране
structureкод конструкцииНеобязательно; должен быть кодом самого плеера, иначе запись получит флаг
screen{"power": ...}Необязательно; состояние дисплея во время показа: on, off или unknown

Как считаются показы

Порядок, повторы и цепочка

Ответ

{"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 одним подписанным пакетом на многих плееров.

Разовая настройка в консоли

Это может сделать только администратор организации оператора, в разделе «Интеграции». Каждый шаг пишется в журнал аудита.

  1. Зарегистрируйте установку CMS: имя, тип и открытый ключ, ключ ECDSA P-256 в виде base64 от DER SubjectPublicKeyInfo. Закрытый ключ никогда не покидает вашу CMS. Консоль покажет идентификатор источника, который входит в каждый пакет.
  2. Сопоставьте каждый плеер CMS, который показывает на одной из ваших конструкций: идентификатор, под которым его знает CMS (player в записях, до 64 символов), и конструкция. Внешний идентификатор используется один раз для установки. Показы плеера без сопоставления отклоняются как player_unknown.
  3. Создайте ключ API с правом write:plays. См. Аутентификация и ключи API.
  4. Чтобы заменить ключ, зарегистрируйте новый открытый ключ для установки. Новый ключ действует сразу; старый остается действительным еще 24 часа, затем отклоняется ответом 401 source_unauthorized.
  5. Если ключ утек, завершите его сразу: 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 и хэш тела храните в собственном хранилище, чтобы после перезапуска нумерация продолжалась, а не повторялась.

Как привести вашу выгрузку к записям

Пределы

Записей в пакете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Когда
400invalid_json, schema_unknownТело не читается, неизвестная schema
400 / 401bad_signatureПодпись не проходит (400 на канале сертификата, где тело хранится отдельно как свидетельство; на остальных 401, ничего не сохраняется)
401device_unauthorized, device_cert_required, stale_requestУчетные данные неизвестны, отозваны или просрочены; ts вне диапазона на подписанном канале
401source_unauthorizedКанал CMS: источник неизвестен или выключен, нет заголовка X-Tanba-Source или ключ заменен больше 24 часов назад
403forbidden, key_scope_deniedКанал CMS: источник принадлежит другой организации, вызывает не ключ API или у ключа нет write:plays
409batch_conflictbatch_seq занят пакетом с другим содержимым
409legacy_after_popПакет в прежнем формате от устройства, которое уже присылало tanba.pop/1; не повторяйте его
413body_too_large, batch_too_largeБольше пределов размера
415unsupported_media_typeКанал CMS: не JSON или кодирование, отличное от gzip
422validation_failedПоля оболочки: batch_seq, формат prev_sha256, пустой events, отсутствующий или не совпадающий source_id
429rate_limitedПревышены пределы частоты

Проверьте, что сохранено

Каждый принятый пакет хранится точными полученными байтами вместе с заголовком подписи, SHA-256 тела и учетными данными, которыми он подписан. Любой, у кого есть открытый ключ, может взять сохраненный пакет, посчитать его хэш, проверить подпись и пройти цепочку по prev_sha256 и номера записей по seq, не доверяя счетам платформы. Как платформа каждый день запечатывает полученную статистику и как проверить печать офлайн, описано в руководстве Как читать выписку и проверять ее доказательства. Программа проверки лежит на странице Загрузки.