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

Передача показов из собственной CMS оператора

POST /api/v1/plays/import

https://app.tanba-ooh.kz/api/v1/plays/import

Право write:plays. Только организации-операторы; сессия консоли администратора оператора тоже подходит. Тело это JSON (application/json, объект с массивом rows) или CSV (text/csv, UTF-8, запятая или точка с запятой, строка заголовков), не больше 5 МБ или 50 000 строк.

Каждая строка называет конструкцию вашей организации (structure_id или structure_code, но не оба), номер согласования, целый час и число показов в этот час. source_ref необязателен и называет источник (например, CMS); он входит в идентичность строки. Часы в формате ISO 8601; час без смещения читается как время Asia/Almaty.

Строка хранится под ключом (организация, конструкция, согласование, час, источник). Повторная отправка заменяет прежнее значение и никогда не прибавляется к нему; строки с разными source_ref хранятся отдельно и складываются. Строки оцениваются по одной: в ответе каждая строка помечена как accepted или refused с кодом причины, а принятые строки сохраняются, даже если другие отклонены. Причины:

ПричинаЗначение
structure_missing, structure_ambiguousНе указано ни structure_id, ни structure_code, либо указаны оба.
structure_not_foundВ вашей организации нет такой конструкции.
approval_missing, approval_not_foundНет номера согласования или нет подписанного согласования с таким номером.
approval_not_for_structureСогласование не покрывает эту конструкцию.
approval_not_valid_at_hourЧас вне срока согласования, до его подписания или после отзыва.
hour_invalid, hour_not_wholeНе час в формате ISO 8601 или не ровно на час.
hour_in_future, hour_too_oldЧас еще не начался или начался больше 62 дней назад.
plays_invalidНе целое число, не меньше 0.
plays_over_capБольше 3600 показов в час на каждый экран конструкции (не меньше одного).
source_ref_invalidДлиннее 100 символов.
duplicate_rowТакая же строка уже была в этом пакете.

Импортированные показы не служат доказательством. Они хранятся отдельно от показов, о которых сообщают плееры, никогда не тарифицируются и никогда не определяют событие контроля. Каждую ночь они сравниваются с отчетными показами (см. reconciliation в /api/v1/stats/reports); если у конструкции, чьи плееры отчитались, импорт расходится с ними больше чем на 10 %, открывается городское событие import_mismatch. Сделать импортированные показы тарифицируемыми можно только отдельным решением платформы, этот интерфейс такого не делает.

Аутентификация

Ключ API в заголовке Authorization или сессия консоли.

Право доступа: write:plays

Тело запроса

application/json

ИмяТипОбязательныйОписаниеПример
rowsarray of objectдаСтроки для хранения: по одной на конструкцию, согласование и час.
rows[].structure_idstring (uuid)Идентификатор конструкции; укажите его или structure_code, но не оба."b7e4a2c9-0d1f-4a68-8e35-91c7f2d40b16"
rows[].structure_codestringКод конструкции; укажите его или structure_id, но не оба."ALM-0012"
rows[].approval_numberstringНомер согласования, которое действовало в этот час."TNB-2026-000123"
rows[].hourstringISO 8601, ровно на час."2026-10-06T14:00:00+05:00"
rows[].playsintegerЧисло показов в этот час, целое число от 0.118
rows[].source_refstringИмя источника, из которого пришла строка, до 100 символов; входит в идентичность строки."cms-main"

Пример

{
  "rows": [
    {
      "structure_code": "ALM-0012",
      "approval_number": "TNB-2026-000123",
      "hour": "2026-10-06T14:00:00+05:00",
      "plays": 118,
      "source_ref": "cms-main"
    },
    {
      "structure_code": "ALM-0012",
      "approval_number": "TNB-2026-000123",
      "hour": "2026-10-06T15:00:00+05:00",
      "plays": 120,
      "source_ref": "cms-main"
    }
  ]
}

text/csv

Пример

structure_code,approval_number,hour,plays,source_ref
ALM-0012,TNB-2026-000123,2026-10-06T14:00:00+05:00,118,cms-main
ALM-0012,TNB-2026-000123,2026-10-06T15:00:00+05:00,120,cms-main

Ответы

200 Пакет прочитан; что сохранено, видно по строкам.

application/json

ИмяТипОбязательныйОписаниеПример
batch_idstring (uuid)даИдентификатор пакета."0b1c3a52-6f0e-4c2e-9d3a-2f5d1e7a9c10"
receivedintegerдаЧисло строк, прочитанных из тела.2
acceptedintegerдаЧисло сохраненных строк.1
refusedintegerдаЧисло отклоненных строк.1
rowsarray of objectдаЗапись на каждую строку, в порядке тела.
rows[].rowintegerдаПозиция в теле, с 1.1
rows[].statusstringдаСохранена ли строка. accepted, refused
rows[].reasonstringЕсть у отклоненных строк."hour_in_future"

Пример

{
  "batch_id": "0b1c3a52-6f0e-4c2e-9d3a-2f5d1e7a9c10",
  "received": 2,
  "accepted": 1,
  "refused": 1,
  "rows": [
    {
      "row": 1,
      "status": "accepted"
    },
    {
      "row": 2,
      "status": "refused",
      "reason": "hour_in_future"
    }
  ]
}

Ошибки

Ошибки приходят документом problem (application/problem+json) со стабильным полем code.

СтатусЗначение
400Ошибка в виде документа problem.
401Нет действительной сессии или ключа API (unauthenticated, session_expired, invalid_api_key).
403Роль или право ключа этого не допускает (forbidden, key_scope_denied, key_route_denied).
413Ошибка в виде документа problem.
415Ошибка в виде документа problem.
422Ошибки полей (validation_failed).
defaultОшибка в виде документа problem.

Примеры кода

curl

curl -X POST 'https://app.tanba-ooh.kz/api/v1/plays/import' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
  "rows": [
    {
      "structure_code": "ALM-0012",
      "approval_number": "TNB-2026-000123",
      "hour": "2026-10-06T14:00:00+05:00",
      "plays": 118,
      "source_ref": "cms-main"
    },
    {
      "structure_code": "ALM-0012",
      "approval_number": "TNB-2026-000123",
      "hour": "2026-10-06T15:00:00+05:00",
      "plays": 120,
      "source_ref": "cms-main"
    }
  ]
}'
Python
import requests

response = requests.post(
    "https://app.tanba-ooh.kz/api/v1/plays/import",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "rows": [
            {
                "structure_code": "ALM-0012",
                "approval_number": "TNB-2026-000123",
                "hour": "2026-10-06T14:00:00+05:00",
                "plays": 118,
                "source_ref": "cms-main",
            },
            {
                "structure_code": "ALM-0012",
                "approval_number": "TNB-2026-000123",
                "hour": "2026-10-06T15:00:00+05:00",
                "plays": 120,
                "source_ref": "cms-main",
            },
        ],
    },
)
print(response.status_code, response.text)
Go
package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "rows": [
    {
      "structure_code": "ALM-0012",
      "approval_number": "TNB-2026-000123",
      "hour": "2026-10-06T14:00:00+05:00",
      "plays": 118,
      "source_ref": "cms-main"
    },
    {
      "structure_code": "ALM-0012",
      "approval_number": "TNB-2026-000123",
      "hour": "2026-10-06T15:00:00+05:00",
      "plays": 120,
      "source_ref": "cms-main"
    }
  ]
}`)
	req, err := http.NewRequest("POST", "https://app.tanba-ooh.kz/api/v1/plays/import", body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
	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))
}