TAŇBA-OOH Developers

Report plays from the operator's own CMS

POST /api/v1/plays/import

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

Scope write:plays. Operator organisations only; a console session of an operator administrator works too. The body is JSON (application/json, an object with a rows array) or CSV (text/csv, UTF-8, comma or semicolon, a header row) and at most 5 MB or 50 000 rows.

Every row names a structure of your organisation (structure_id or structure_code, not both), an approval number, a whole hour and the number of plays in that hour. source_ref is optional and names the feed (for example the CMS); it is part of the identity of a row. Hours are ISO 8601; one without an offset is read as Asia/Almaty time.

A row is stored under (organisation, structure, approval, hour, source). Sending it again replaces the earlier value and never adds to it; rows with different source_ref values are separate and add up. Rows are judged one by one: the answer lists every row as accepted or refused with a reason code, and the accepted rows are stored even when others are refused. Reasons:

ReasonMeaning
structure_missing, structure_ambiguousNeither or both of structure_id and structure_code were given.
structure_not_foundNo such structure in your organisation.
approval_missing, approval_not_foundNo approval number, or no signed approval with that number.
approval_not_for_structureThe approval does not cover that structure.
approval_not_valid_at_hourThe hour is outside the approval's period, before it was signed, or after it was revoked.
hour_invalid, hour_not_wholeNot an ISO 8601 hour, or not on the hour.
hour_in_future, hour_too_oldThe hour has not begun, or began more than 62 days ago.
plays_invalidNot a whole number of at least 0.
plays_over_capMore than 3600 plays an hour for each screen of the structure (at least one).
source_ref_invalidLonger than 100 characters.
duplicate_rowThe same row appeared earlier in this batch.

Imported plays are not evidence. They are kept apart from the plays the players report, they are never billed, and they never decide a compliance event. Each night they are compared with the reported plays (see reconciliation in /api/v1/stats/reports); a structure whose players reported and whose import differs by more than 10 % raises an import_mismatch city event. Making imported plays billable would need an explicit decision of the platform and is not part of this interface.

Authentication

API key in the Authorization header, or a console session.

Scope: write:plays

Request body

application/json

NameTypeRequiredDescriptionExample
rowsarray of objectyesThe rows to store, one for each structure, approval and hour.
rows[].structure_idstring (uuid)Identifier of the structure; give this or structure_code, not both."b7e4a2c9-0d1f-4a68-8e35-91c7f2d40b16"
rows[].structure_codestringCode of the structure; give this or structure_id, not both."ALM-0012"
rows[].approval_numberstringNumber of the approval that was in force in that hour."TNB-2026-000123"
rows[].hourstringISO 8601, on the hour."2026-10-06T14:00:00+05:00"
rows[].playsintegerNumber of plays in that hour, a whole number of 0 or more.118
rows[].source_refstringName of the feed the row comes from, up to 100 characters; part of the identity of the row."cms-main"

Example

{
  "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

Example

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

Responses

200 The batch was read; see the rows for what was stored.

application/json

NameTypeRequiredDescriptionExample
batch_idstring (uuid)yesIdentifier of the batch."0b1c3a52-6f0e-4c2e-9d3a-2f5d1e7a9c10"
receivedintegeryesNumber of rows read from the body.2
acceptedintegeryesNumber of rows stored.1
refusedintegeryesNumber of rows refused.1
rowsarray of objectyesOne entry for each row, in the order of the body.
rows[].rowintegeryesPosition in the body, from 1.1
rows[].statusstringyesWhether the row was stored. accepted, refused
rows[].reasonstringPresent on refused rows."hour_in_future"

Example

{
  "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"
    }
  ]
}

Errors

Errors are problem documents (application/problem+json) with a stable code.

StatusMeaning
400An error as a problem document.
401No valid session or API key (unauthenticated, session_expired, invalid_api_key).
403The role or the key's scope does not allow this (forbidden, key_scope_denied, key_route_denied).
413An error as a problem document.
415An error as a problem document.
422Field errors (validation_failed).
defaultAn error as a problem document.

Code samples

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))
}