Signed batch of play events
POST /d/v1/telemetry
https://devices.tanba-ooh.kz/d/v1/telemetry
A tanba.pop/1 batch on the certificate channel. The body is JSON signed with the device key in the X-Tanba-Signature header (ES256, r||s, base64url, over the exact bytes of the body); the call presents the client certificate. The batch carries batch_seq, prev_sha256 (SHA-256 of the previous batch body, empty for the first), sent_at and events; each event may carry seq, a number that runs on by one per record across batches. The platform keeps the body exactly as received. The same batch_seq with the same bytes is answered duplicate: true; with other bytes it is refused 409 batch_conflict. Records at or below the highest number already taken are not counted again; a jump in the numbers and a prev_sha256 that does not match are accepted and recorded.
A batch whose signature fails is never counted: it is kept apart as evidence and answered 400 bad_signature. An unknown schema is 400 schema_unknown. A body without schema is the earlier format (play_start and play_end events), accepted as before; once a tanba.pop/1 batch of a device has been accepted, the device can no longer send the earlier format (409 legacy_after_pop). In a tanba.pop/1 batch a bad record is refused and listed in the answer; the rest of the batch counts. The records, flags and refusal reasons are in the guide "Send proof of play from your players or CMS".
Authentication
Client certificate of the device (mutual TLS) and a signature of the body by the device key.
Parameters
| Name | In | Type | Required | Description | Example |
|---|---|---|---|---|---|
X-Tanba-Signature | header | string | yes | ES256 signature of the exact bytes of the body by the device key, raw r||s (64 bytes), base64url without padding. | q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw |
Request body
application/json
PopDeviceBatch
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
schema | string | yes | Format of the batch. tanba.pop/1 | |
device_id | string (uuid) | Signed channel only: the device that sends the batch; on the certificate channel the device is the one in the certificate. | "5c0e8f3a-71d2-4b6e-9a14-d8e2f7b30c59" | |
ts | string (date-time) | Signed channel only: the clock of the agent, within 300 seconds of the clock of the platform. | "2026-10-14T09:05:00.120Z" | |
batch_seq | integer | yes | Number of the batch for this device; taken once. | 1042 |
prev_sha256 | string | Hex SHA-256 of the body of the previous batch; empty for the first. | "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123" | |
sent_at | string (date-time) | When the player cut the batch. | "2026-10-14T09:05:00.120Z" | |
events | array of object | yes | The records of the batch in order; at most 1000. | |
events[].kind | string | yes | Kind of the record. A finished play is play; reject is a file the player refused to show, mode_change a change of state and tamper a broken local log. play, reject, mode_change, tamper | |
events[].seq | integer | Number of the record for this player, +1 for every record across batches, from 1; required on play. | 18 | |
events[].play_id | string | Identifier of the play, unique for the player; characters A-Za-z0-9_-. | "7c1e9a40b2d35f68" | |
events[].approval_number | string | Number of the approval that was played; required on a play unless slot is set. | "TNB-2026-000123" | |
events[].slot | string | Authority content: no approval number, never billed, counted apart. gov, emergency | ||
events[].media_sha256 | string | SHA-256 of the file played, 64 lowercase hex characters; required on a play. | "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123" | |
events[].start | string (date-time) | When the play began, RFC 3339 with milliseconds. | "2026-10-14T09:04:50.000Z" | |
events[].end | string (date-time) | When the play ended; start is not after it. | "2026-10-14T09:05:00.000Z" | |
events[].duration_ms | integer | Time on screen in milliseconds; a play counts from 1000. | 10000 | |
events[].structure | string | Code of the structure the player believes it stands at; a different one is flagged. | "ALM-0012" | |
events[].screen | object | State of the display during the play. | ||
events[].screen.power | string | Whether the display was on. on, off, unknown | ||
events[].at | string (date-time) | When the event happened; on reject, mode_change and tamper. | "2026-10-14T09:00:00.000Z" | |
events[].code | string | Why the file was refused; on reject. NO_SIGNATURE, BAD_SIGNATURE, HASH_MISMATCH, EXPIRED_APPROVAL, REVOKED | ||
events[].detail | object | Further facts of a reject, mode_change or tamper record. On a play it may carry src, the attribution tag of a programmatic play: {channel, seller_id, deal_id, imp_id} with channel a lowercase word, seller_id the id of the selling platform's organisation (seller_id of authorised_sellers), deal_id and imp_id optional (A-Za-z0-9._:-, up to 64). A tag of any other shape is ignored; the play counts all the same. A tag is attribution, not proof, and never changes billing. |
Example
{
"schema": "tanba.pop/1",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"sent_at": "2026-10-14T09:05:00.120Z",
"events": [
{
"kind": "play",
"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,
"screen": {
"power": "on"
}
}
]
}
Responses
200 Recorded; see `items` for the records that were not plainly accepted.
application/json
PopAnswer
| Name | Type | Required | Description | Example |
|---|---|---|---|---|
batch_seq | integer | yes | Number of the batch. | 1042 |
received | integer | yes | Number of records read from the body. | 12 |
accepted | integer | yes | Number of records counted, flagged ones included. | 10 |
duplicates | integer | yes | Number of records at or below the highest number already taken for their player. | 0 |
refused | integer | yes | Number of records refused. | 2 |
flagged | integer | yes | Number of accepted records that carry a flag. | 1 |
duplicate | boolean | yes | True when this very batch was received before; nothing was counted again. | false |
items | array of object | yes | Only the records that are not plainly accepted; a clean batch gets an empty list. | |
items[].index | integer | yes | Position in events, from 0. | 3 |
items[].status | string | yes | What became of the record. duplicate, refused, flagged | |
items[].reason | string | Present on refused records, for example time_in_future. | "time_in_future" | |
items[].flags | array of string | Present on flagged records; the flags are listed in the guide on sending proof of play. |
Example
{
"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": "time_in_future"
}
]
}
Errors
Errors are problem documents (application/problem+json) with a stable code.
| Status | Meaning |
|---|---|
| 400 | The signature does not verify (bad_signature), the body is not JSON (invalid_json) or the schema is unknown (schema_unknown). |
| 401 | The device is unknown, revoked or expired (device_unauthorized), or no certificate was presented (device_cert_required). |
| 409 | The batch_seq was taken by a batch with other content (batch_conflict), or the batch is in the earlier format from a device that has had a tanba.pop/1 batch accepted (legacy_after_pop; do not retry it). |
| 413 | More than 4 MB or 1000 events (body_too_large, batch_too_large). |
| 422 | Field errors (validation_failed). |
| 429 | More than one batch every 10 seconds for the device (a burst of 3 is allowed). Retry-After gives the seconds to wait; resend the same bytes (rate_limited). |
| default | An error as a problem document. |
Code samples
curl
curl -X POST 'https://devices.tanba-ooh.kz/d/v1/telemetry' \
-H 'X-Tanba-Signature: q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw' \
-H 'Content-Type: application/json' \
-d '{
"schema": "tanba.pop/1",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"sent_at": "2026-10-14T09:05:00.120Z",
"events": [
{
"kind": "play",
"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,
"screen": {
"power": "on"
}
}
]
}'
Python
import requests
response = requests.post(
"https://devices.tanba-ooh.kz/d/v1/telemetry",
headers={
"X-Tanba-Signature": "q3J2y0uO8e1m4n7pXc5Zk9d2Wv6Tg0hLsB-aRfN1YjE6uHxPzD3oVtKiC8wMbQe2nSgA4rUl7yFhJ9XcT0dZaw",
"Content-Type": "application/json",
},
json={
"schema": "tanba.pop/1",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"sent_at": "2026-10-14T09:05:00.120Z",
"events": [
{
"kind": "play",
"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,
"screen": {
"power": "on",
},
},
],
},
)
print(response.status_code, response.text)
Go
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{
"schema": "tanba.pop/1",
"batch_seq": 1042,
"prev_sha256": "9f2b6a41c7d83e05a1b4c6d7e8f90123456789abcdef0123456789abcdef0123",
"sent_at": "2026-10-14T09:05:00.120Z",
"events": [
{
"kind": "play",
"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,
"screen": {
"power": "on"
}
}
]
}`)
req, err := http.NewRequest("POST", "https://devices.tanba-ooh.kz/d/v1/telemetry", body)
if err != nil {
panic(err)
}
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))
}