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:
| Reason | Meaning |
|---|---|
structure_missing, structure_ambiguous | Neither or both of structure_id and structure_code were given. |
structure_not_found | No such structure in your organisation. |
approval_missing, approval_not_found | No approval number, or no signed approval with that number. |
approval_not_for_structure | The approval does not cover that structure. |
approval_not_valid_at_hour | The hour is outside the approval's period, before it was signed, or after it was revoked. |
hour_invalid, hour_not_whole | Not an ISO 8601 hour, or not on the hour. |
hour_in_future, hour_too_old | The hour has not begun, or began more than 62 days ago. |
plays_invalid | Not a whole number of at least 0. |
plays_over_cap | More than 3600 plays an hour for each screen of the structure (at least one). |
source_ref_invalid | Longer than 100 characters. |
duplicate_row | The 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
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
rows | array of object | yes | The rows to store, one for each structure, approval and hour. | |
rows[].structure_id | string (uuid) | Identifier of the structure; give this or structure_code, not both. | "b7e4a2c9-0d1f-4a68-8e35-91c7f2d40b16" | |
rows[].structure_code | string | Code of the structure; give this or structure_id, not both. | "ALM-0012" | |
rows[].approval_number | string | Number of the approval that was in force in that hour. | "TNB-2026-000123" | |
rows[].hour | string | ISO 8601, on the hour. | "2026-10-06T14:00:00+05:00" | |
rows[].plays | integer | Number of plays in that hour, a whole number of 0 or more. | 118 | |
rows[].source_ref | string | Name 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
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
batch_id | string (uuid) | yes | Identifier of the batch. | "0b1c3a52-6f0e-4c2e-9d3a-2f5d1e7a9c10" |
received | integer | yes | Number of rows read from the body. | 2 |
accepted | integer | yes | Number of rows stored. | 1 |
refused | integer | yes | Number of rows refused. | 1 |
rows | array of object | yes | One entry for each row, in the order of the body. | |
rows[].row | integer | yes | Position in the body, from 1. | 1 |
rows[].status | string | yes | Whether the row was stored. accepted, refused | |
rows[].reason | string | Present 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.
| Status | Meaning |
|---|---|
| 400 | An error as a problem document. |
| 401 | No valid session or API key (unauthenticated, session_expired, invalid_api_key). |
| 403 | The role or the key's scope does not allow this (forbidden, key_scope_denied, key_route_denied). |
| 413 | An error as a problem document. |
| 415 | An error as a problem document. |
| 422 | Field errors (validation_failed). |
| default | An 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))
}