Signed agent telemetry batch
POST /s/v1/telemetry
https://agents.tanba-ooh.kz/s/v1/telemetry
A tanba.pop/1 batch on the signed channel, for agents that have no client certificate. The body is JSON with device_id and ts (the clock of the agent, within 300 seconds of the clock of the platform), signed with the key registered at enrolment in the X-Tanba-Signature header (ES256, r||s, base64url, over the exact bytes of the body). The rules after that are those of the certificate channel: batch_seq, prev_sha256, sent_at, events, the record numbers seq, the body kept as received, repeats and conflicts.
The body is at most 1 MB, and a device may send 10 batches a minute. A signature that does not verify is 401 bad_signature and nothing is stored.
Authentication
Signature of the body by the key registered at enrolment. No certificate.
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 body is not JSON (invalid_json) or the schema is unknown (schema_unknown). |
| 401 | The signature does not verify (bad_signature), the device is unknown or revoked (device_unauthorized), or ts is out of range (stale_request). |
| 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 1 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://agents.tanba-ooh.kz/s/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://agents.tanba-ooh.kz/s/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://agents.tanba-ooh.kz/s/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))
}