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

Передача показов из 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, если тело сжато. gzipgzip

Тело запроса

application/json

ИмяТипОбязательныйОписаниеПример
schemastringдаФормат пакета. tanba.pop/1
source_idstring (uuid)даИсточник внутри подписанных байтов; должен совпадать с X-Tanba-Source."3f6c1d2a-8b47-4e19-a5d0-7c2e9b14f836"
batch_seqintegerдаНомер пакета для этой установки; занимается один раз.1042
prev_sha256stringSHA-256 тела предыдущего пакета в hex; для первого пусто."9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123"
sent_atstring (date-time)Когда CMS закрыла пакет."2026-10-14T09:05:00.120Z"
recordsarray of objectдаПо записи на каждый завершенный показ, в порядке, в котором плееры о них сообщили.
records[].kindstringдаИз CMS принимаются только показы. play
records[].playerstringдаВнешний идентификатор, под которым плеер сопоставлен в консоли."1001:55:2"
records[].seqintegerдаНомер записи для этого плеера, +1 для каждой записи через границы пакетов, с 1.18
records[].play_idstringдаИдентификатор показа, уникальный для плеера; символы A-Za-z0-9_-."7c1e9a40b2d35f68"
records[].approval_numberstringНомер показанного согласования; обязателен, если не задан slot."TNB-2026-000123"
records[].slotstringМатериал уполномоченного органа: без номера согласования, никогда не тарифицируется. gov, emergency
records[].media_sha256stringдаSHA-256 показанного файла, 64 символа hex в нижнем регистре."9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123"
records[].startstring (date-time)даКогда показ начался, RFC 3339."2026-10-14T09:04:50.000Z"
records[].endstring (date-time)даКогда показ закончился, RFC 3339."2026-10-14T09:05:00.000Z"
records[].duration_msintegerдаВремя на экране в миллисекундах.10000
records[].structurestringКод конструкции, на которой, по мнению плеера, он стоит; другой код получает флаг."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_seqintegerдаНомер пакета.1042
receivedintegerдаЧисло записей, прочитанных из тела.12
acceptedintegerдаЧисло засчитанных записей, включая записи с флагами.10
duplicatesintegerдаЧисло записей с номером не выше уже принятого для их плеера.0
refusedintegerдаЧисло отклоненных записей.2
flaggedintegerдаЧисло принятых записей с флагом.1
duplicatebooleanдаTrue, если этот же пакет уже получали; повторно ничего не засчитано.false
itemsarray of objectдаТолько записи, принятые не безоговорочно; у чистого пакета список пуст.
items[].indexintegerдаПозиция в records, с 0.3
items[].statusstringдаЧто стало с записью. duplicate, refused, flagged
items[].reasonstringЕсть у отклоненных записей, например player_unknown или time_in_future."player_unknown"
items[].flagsarray 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))
}