Передача показов из CMS: один подписанный пакет для многих плееров
POST /api/v1/pop/batches
https://app.tanba-ooh.kz/api/v1/pop/batches
Право write:plays. Только организации-операторы. Пакет отправляется с ключом API и подписью: установка CMS регистрируется один раз в консоли (имя, вид и открытый ключ ECDSA P-256 в виде base64 от SubjectPublicKeyInfo), каждый ее плеер сопоставляется там с конструкцией организации, а каждый пакет называет установку в X-Tanba-Source и несет в X-Tanba-Signature подпись ES256 (сырые r||s, base64url без заполнения) точных байтов тела. Тело можно отправить с Content-Encoding: gzip; подпись тогда покрывает байты после распаковки.
Тело это tanba.pop/1, где вместо events стоит records: schema, batch_seq (целое от 0, занимается один раз для установки), prev_sha256 (SHA-256 тела пакета batch_seq - 1, для первого пусто), sent_at и records. Запись это play, как в руководстве по передаче показов, плюс player, внешний идентификатор, под которым плеер сопоставлен; seq растет на единицу для каждого плеера через границы пакетов. Записи других видов отклоняются. Не больше 10 000 записей, 4 МБ в пути и 16 МБ после распаковки (сжатое тело не может раздуваться больше чем в 20 раз).
Записи оцениваются по одной. Запись отклоняется, попадает в items и не засчитывается, если плеер не сопоставлен (или сопоставление удалено), с причиной player_unknown, либо по причинам из руководства; остальной пакет считается. Счета сохраняются на сопоставленном плеере как подтвержденные оператором (attested) показы: о них сообщила система оператора и подписала своим ключом, устройство их не видело. Тот же batch_seq с теми же байтами получает ответ duplicate: true и считается один раз; с другими байтами отклоняется как 409 batch_conflict. Записи с номером не выше уже принятого для их плеера повторно не считаются.
Пределы: 2 пакета в секунду на установку (всплеск 10), 10 вызовов в секунду на ключ (всплеск 30) и 2 000 000 записей в час на организацию; сверх них ответ 429 rate_limited с Retry-After. Ключ, который заменили, принимается еще 24 часа после регистрации нового, потом ответ 401 source_unauthorized. Подпись, которая не проходит, дает 401 bad_signature, и ничего не сохраняется.
Аутентификация
Ключ API в заголовке Authorization.
Право доступа: write:plays
Параметры
| Имя | Где | Тип | Обязательный | Описание | Пример |
|---|---|---|---|---|---|
X-Tanba-Source | заголовок | string (uuid) | да | Идентификатор зарегистрированной установки CMS, которая подписала пакет. | 3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836 |
X-Tanba-Signature | заголовок | string | да | Подпись ES256 тела (после распаковки), сырые r||s, base64url без заполнения. | q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw |
Content-Encoding | заголовок | string | нет | gzip, если тело сжато. gzip | gzip |
Тело запроса
application/json
| Имя | Тип | Обязательный | Описание | Пример |
|---|---|---|---|---|
schema | string | да | Формат пакета. tanba.pop/1 | |
source_id | string (uuid) | да | Источник внутри подписанных байтов; должен совпадать с X-Tanba-Source. | "3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836" |
batch_seq | integer | да | Номер пакета для этой установки; занимается один раз. | 1042 |
prev_sha256 | string | SHA-256 тела предыдущего пакета в hex; для первого пусто. | "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123" | |
sent_at | string (date-time) | Когда CMS закрыла пакет. | "2026-10-14T09:05:00.120Z" | |
records | array of object | да | По записи на каждый завершенный показ, в порядке, в котором плееры о них сообщили. | |
records[].kind | string | да | Из CMS принимаются только показы. play | |
records[].player | string | да | Внешний идентификатор, под которым плеер сопоставлен в консоли. | "1001:55:2" |
records[].seq | integer | да | Номер записи для этого плеера, +1 для каждой записи через границы пакетов, с 1. | 18 |
records[].play_id | string | да | Идентификатор показа, уникальный для плеера; символы A-Za-z0-9_-. | "7c1e9a40b2d35f68" |
records[].approval_number | string | Номер показанного согласования; обязателен, если не задан slot. | "TNB-2026-000123" | |
records[].slot | string | Материал уполномоченного органа: без номера согласования, никогда не тарифицируется. gov, emergency | ||
records[].media_sha256 | string | да | SHA-256 показанного файла, 64 символа hex в нижнем регистре. | "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123" |
records[].start | string (date-time) | да | Когда показ начался, RFC 3339. | "2026-10-14T09:04:50.000Z" |
records[].end | string (date-time) | да | Когда показ закончился, RFC 3339. | "2026-10-14T09:05:00.000Z" |
records[].duration_ms | integer | да | Время на экране в миллисекундах. | 10000 |
records[].structure | string | Код конструкции, на которой, по мнению плеера, он стоит; другой код получает флаг. | "ALM-0012" |
Пример
{
"schema": "tanba.pop/1",
"source_id": "3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"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
}
]
}
Ответы
200 Пакет прочитан; записи, принятые не безоговорочно, перечислены в `items`.
application/json
| Имя | Тип | Обязательный | Описание | Пример |
|---|---|---|---|---|
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 | да | Позиция в records, с 0. | 3 |
items[].status | string | да | Что стало с записью. duplicate, refused, flagged | |
items[].reason | string | Есть у отклоненных записей, например player_unknown или time_in_future. | "player_unknown" | |
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": "player_unknown"
}
]
}
Ошибки
Ошибки приходят документом problem (application/problem+json) со стабильным полем code.
| Статус | Значение |
|---|---|
| 401 | Нет действительной сессии или ключа API (unauthenticated, session_expired, invalid_api_key). |
| 403 | Роль или право ключа этого не допускает (forbidden, key_scope_denied, key_route_denied). |
| 409 | Номер batch_seq уже занят пакетом с другим содержимым (batch_conflict). |
| 413 | Тело больше пределов размера (body_too_large, batch_too_large). |
| 415 | Не JSON или кодирование, отличное от gzip (unsupported_media_type). |
| 422 | Ошибки полей (validation_failed). |
| 429 | Превышен предел частоты; см. Retry-After. |
| default | Ошибка в виде документа problem. |
Примеры кода
curl
curl -X POST 'https://app.tanba-ooh.kz/api/v1/pop/batches' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'X-Tanba-Source: 3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836' \
-H 'X-Tanba-Signature: q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw' \
-H 'Content-Type: application/json' \
-d '{
"schema": "tanba.pop/1",
"source_id": "3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"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
}
]
}'
Python
import requests
response = requests.post(
"https://app.tanba-ooh.kz/api/v1/pop/batches",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"X-Tanba-Source": "3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836",
"X-Tanba-Signature": "q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw",
"Content-Type": "application/json",
},
json={
"schema": "tanba.pop/1",
"source_id": "3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"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,
},
],
},
)
print(response.status_code, response.text)
Go
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{
"schema": "tanba.pop/1",
"source_id": "3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"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
}
]
}`)
req, err := http.NewRequest("POST", "https://app.tanba-ooh.kz/api/v1/pop/batches", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
req.Header.Set("X-Tanba-Source", "3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836")
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))
}