Подписанный пакет событий показов
POST /d/v1/telemetry
https://devices.tanba-ooh.kz/d/v1/telemetry
Пакет tanba.pop/1 на канале сертификата. Тело это JSON, подписанный ключом устройства в заголовке X-Tanba-Signature (ES256, r||s, base64url, по точным байтам тела); вызов предъявляет клиентский сертификат. Пакет несет batch_seq, prev_sha256 (SHA-256 тела предыдущего пакета, для первого пусто), sent_at и events; у каждого события может быть seq, число, которое растет на единицу для каждой записи через границы пакетов. Платформа хранит тело ровно так, как получила. Тот же batch_seq с теми же байтами получает ответ duplicate: true; с другими байтами отклоняется как 409 batch_conflict. Записи с номером не выше уже принятого не считаются повторно; скачок номеров и несовпавший prev_sha256 принимаются и записываются.
Пакет, чья подпись не проходит, никогда не засчитывается: он хранится отдельно как свидетельство, ответ 400 bad_signature. Неизвестная schema дает 400 schema_unknown. Тело без schema это прежний формат (события play_start и play_end), принимается как раньше; после первого принятого пакета tanba.pop/1 устройство больше не может слать прежний формат (409 legacy_after_pop). Плохая запись в пакете tanba.pop/1 отклоняется и перечисляется в ответе, остальной пакет считается. Подробности записей, флагов и причин отказа в руководстве «Передача показов с плееров и из CMS».
Аутентификация
Клиентский сертификат устройства (взаимный TLS) и подпись тела ключом устройства.
Параметры
| Имя | Где | Тип | Обязательный | Описание | Пример |
|---|---|---|---|---|---|
X-Tanba-Signature | заголовок | string | да | Подпись ES256 точных байтов тела ключом устройства, сырые r||s (64 байта), base64url без заполнения. | q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw |
Тело запроса
application/json
PopDeviceBatch
| Имя | Тип | Обязательный | Описание | Пример |
|---|---|---|---|---|
schema | string | да | Формат пакета. tanba.pop/1 | |
device_id | string (uuid) | Только подписанный канал: устройство, которое шлет пакет; на канале сертификата устройство определяется по сертификату. | "5c0e8f3a-71d2-4b6e-9a14-d8e2f7b30c59" | |
ts | string (date-time) | Только подписанный канал: часы агента, не дальше 300 секунд от часов платформы. | "2026-10-14T09:05:00.120Z" | |
batch_seq | integer | да | Номер пакета для этого устройства; занимается один раз. | 1042 |
prev_sha256 | string | SHA-256 тела предыдущего пакета в hex; для первого пусто. | "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123" | |
sent_at | string (date-time) | Когда плеер закрыл пакет. | "2026-10-14T09:05:00.120Z" | |
events | array of object | да | Записи пакета по порядку; не больше 1000. | |
events[].kind | string | да | Вид записи. Завершенный показ это play; reject файл, который плеер отказался показывать, mode_change смена состояния, tamper нарушенный локальный журнал. play, reject, mode_change, tamper | |
events[].seq | integer | Номер записи для этого плеера, +1 для каждой записи через границы пакетов, с 1; обязателен для play. | 18 | |
events[].play_id | string | Идентификатор показа, уникальный для плеера; символы A-Za-z0-9_-. | "7c1e9a40b2d35f68" | |
events[].approval_number | string | Номер показанного согласования; обязателен для play, если не задан slot. | "TNB-2026-000123" | |
events[].slot | string | Материал уполномоченного органа: без номера согласования, никогда не тарифицируется, считается отдельно. gov, emergency | ||
events[].media_sha256 | string | SHA-256 показанного файла, 64 символа hex в нижнем регистре; обязателен для play. | "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123" | |
events[].start | string (date-time) | Когда показ начался, RFC 3339 с миллисекундами. | "2026-10-14T09:04:50.000Z" | |
events[].end | string (date-time) | Когда показ закончился; start не позже него. | "2026-10-14T09:05:00.000Z" | |
events[].duration_ms | integer | Время на экране в миллисекундах; показ засчитывается от 1000. | 10000 | |
events[].structure | string | Код конструкции, на которой, по мнению плеера, он стоит; другой код получает флаг. | "ALM-0012" | |
events[].screen | object | Состояние дисплея во время показа. | ||
events[].screen.power | string | Был ли дисплей включен. on, off, unknown | ||
events[].at | string (date-time) | Когда произошло событие; для reject, mode_change и tamper. | "2026-10-14T09:00:00.000Z" | |
events[].code | string | Почему файл отклонен; для reject. NO_SIGNATURE, BAD_SIGNATURE, HASH_MISMATCH, EXPIRED_APPROVAL, REVOKED | ||
events[].detail | object | Дополнительные сведения записей reject, mode_change и tamper. В записи play может быть src, метка источника программатик-показа: {channel, seller_id, deal_id, imp_id}, где channel слово из строчных латинских букв, seller_id идентификатор организации продающей платформы (seller_id из authorised_sellers), deal_id и imp_id необязательны (A-Za-z0-9._:-, до 64). Метка другой формы игнорируется, показ засчитывается. Метка это указание источника, а не доказательство, и тарификацию не меняет. |
Пример
{
"schema": "tanba.pop/1",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"sent_at": "2026-10-14T09:05:00.120Z",
"events": [
{
"kind": "play",
"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,
"screen": {
"power": "on"
}
}
]
}
Ответы
200 Записано; записи, принятые не безоговорочно, перечислены в `items`.
application/json
PopAnswer
| Имя | Тип | Обязательный | Описание | Пример |
|---|---|---|---|---|
batch_seq | integer | да | Номер пакета. | 1042 |
received | integer | да | Число записей, прочитанных из тела. | 12 |
accepted | integer | да | Число засчитанных записей, включая записи с флагами. | 10 |
duplicates | integer | да | Число записей с номером не выше уже принятого для их плеера. | 0 |
refused | integer | да | Число отклоненных записей. | 2 |
flagged | integer | да | Число принятых записей с флагом. | 1 |
duplicate | boolean | да | True, если этот же пакет уже получали; повторно ничего не засчитано. | false |
items | array of object | да | Только записи, принятые не безоговорочно; у чистого пакета список пуст. | |
items[].index | integer | да | Позиция в events, с 0. | 3 |
items[].status | string | да | Что стало с записью. duplicate, refused, flagged | |
items[].reason | string | Есть у отклоненных записей, например time_in_future. | "time_in_future" | |
items[].flags | array of string | Есть у записей с флагами; флаги перечислены в руководстве по передаче показов. |
Пример
{
"batch_seq": 1042,
"received": 12,
"accepted": 10,
"duplicates": 0,
"refused": 2,
"flagged": 1,
"duplicate": false,
"items": [
{
"index": 3,
"status": "flagged",
"flags": [
"incomplete"
]
},
{
"index": 7,
"status": "refused",
"reason": "time_in_future"
}
]
}
Ошибки
Ошибки приходят документом problem (application/problem+json) со стабильным полем code.
| Статус | Значение |
|---|---|
| 400 | Подпись не проходит (bad_signature), тело не JSON (invalid_json) или schema неизвестна (schema_unknown). |
| 401 | Устройство неизвестно, отозвано или просрочено (device_unauthorized) либо сертификат не предъявлен (device_cert_required). |
| 409 | Номер batch_seq уже занят пакетом с другим содержимым (batch_conflict) либо пакет в прежнем формате пришел от устройства, у которого уже принят пакет tanba.pop/1 (legacy_after_pop; не повторяйте его). |
| 413 | Больше 4 МБ или 1000 событий (body_too_large, batch_too_large). |
| 422 | Ошибки полей (validation_failed). |
| 429 | Больше одного пакета в 10 секунд для устройства (допустимы 3 подряд). В Retry-After указано, сколько секунд ждать; повторите те же байты (rate_limited). |
| default | Ошибка в виде документа problem. |
Примеры кода
curl
curl -X POST 'https://devices.tanba-ooh.kz/d/v1/telemetry' \
-H 'X-Tanba-Signature: q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw' \
-H 'Content-Type: application/json' \
-d '{
"schema": "tanba.pop/1",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"sent_at": "2026-10-14T09:05:00.120Z",
"events": [
{
"kind": "play",
"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,
"screen": {
"power": "on"
}
}
]
}'
Python
import requests
response = requests.post(
"https://devices.tanba-ooh.kz/d/v1/telemetry",
headers={
"X-Tanba-Signature": "q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw",
"Content-Type": "application/json",
},
json={
"schema": "tanba.pop/1",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"sent_at": "2026-10-14T09:05:00.120Z",
"events": [
{
"kind": "play",
"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,
"screen": {
"power": "on",
},
},
],
},
)
print(response.status_code, response.text)
Go
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{
"schema": "tanba.pop/1",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"sent_at": "2026-10-14T09:05:00.120Z",
"events": [
{
"kind": "play",
"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,
"screen": {
"power": "on"
}
}
]
}`)
req, err := http.NewRequest("POST", "https://devices.tanba-ooh.kz/d/v1/telemetry", 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))
}