@trillboards/api-client 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-AMOSJA6G.mjs → chunk-EIB4SNHX.mjs} +2 -2
- package/dist/chunk-EIB4SNHX.mjs.map +1 -0
- package/dist/generated.d.mts +644 -3
- package/dist/generated.d.ts +644 -3
- package/dist/generated.js.map +1 -1
- package/dist/generated.mjs +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
- package/dist/chunk-AMOSJA6G.mjs.map +0 -1
package/dist/generated.d.mts
CHANGED
|
@@ -17,6 +17,7 @@ type Ad = {
|
|
|
17
17
|
impression_id?: string;
|
|
18
18
|
/** **Deprecated alias of `tracking.impression_url`.** These two fields are the same string, byte for byte, produced from one value server-side — they are not two different hooks and firing both records the same call twice. Read `tracking.impression_url`; this flat field is retained only for clients that predate the `tracking` object and will be removed once they are off it. */
|
|
19
19
|
impression_url?: string;
|
|
20
|
+
/** The advertiser's own creative label from the booking record (its `title`, falling back to its `name`), carried through for your logs and QA. Omitted when the booking has neither. It is an internal label, not reviewed display copy, so do not surface it to viewers. */
|
|
20
21
|
title?: string;
|
|
21
22
|
/** Structured proof-of-play URLs for clients that prefer named tracking hooks over legacy flat fields. **Fire exactly one of `impression_url` / `complete_url` per play.** They resolve to the same endpoint and differ only by the `completed` query parameter, which that endpoint does not act on (see `GET /impression`) — calling both records the play twice. */
|
|
22
23
|
tracking?: {
|
|
@@ -27,6 +28,7 @@ type Ad = {
|
|
|
27
28
|
/** **Server-signed proof of play. Present only on owner-billable direct bookings; absent on self-promo, house/QR, demo and fully-delivered placements.** When present, fire it VERBATIM — including the `sig`, `request_id` and `impid` parameters — once the ad has finished playing. Do not rewrite, re-sign, re-order, or drop any query parameter: the signature covers the values and any edit invalidates it. This is the only completion signal on the `data.ads` rail that settles; `complete_url` above is delivery accounting and is not billing-grade. Replays of the same play are collapsed on the SIGNED `request_id`, which carries this serve's `impression_id`. Because it is covered by `sig`, a replay that alters it to claim a fresh dedup key fails verification and records nothing — one served play can settle at most once. Firing it in addition to `complete_url` is correct and does not double-count — they land in different stores. */
|
|
28
29
|
owned_direct_complete_url?: string;
|
|
29
30
|
};
|
|
31
|
+
/** `video` or `image`. On the partner/IMA rail an image instant-ad creative is substituted with its pre-rendered MP4 and reports `video`; seeing `image` here means that MP4 was not ready on this poll, so a video-only player should skip the entry and pick it up on a later poll rather than treat it as a still. */
|
|
30
32
|
type?: "video" | "image";
|
|
31
33
|
/** Media URL (CDN) */
|
|
32
34
|
url?: string;
|
|
@@ -37,8 +39,11 @@ type AdSettings = {
|
|
|
37
39
|
content_fit?: ContentFitSettings;
|
|
38
40
|
/** Seconds to display image ads; independent from ad rotation cadence. */
|
|
39
41
|
image_duration?: number;
|
|
42
|
+
/** An ADVISORY delivery cap the client SDK is expected to honour — the server reports it and never enforces it anywhere. Sourced from the partner's `network_config.ad_delivery.max_ads_per_hour` (an integer 1-120), falling back to `sdk_config.max_ads_per_hour`; when neither is set the key is ABSENT from the response rather than null or defaulted. */
|
|
40
43
|
max_ads_per_hour?: number;
|
|
44
|
+
/** Label for the tap-to-continue overlay the partner Lite SDK draws over an ad. Read from `sdk_config.overlay_button_text` only — there is no runtime-config path for it — and the key is ABSENT when unset, in which case the SDK falls back to `TAP TO CONTINUE` client-side. */
|
|
41
45
|
overlay_button_text?: string;
|
|
46
|
+
/** Whether ad audio plays. It reads the typed `sound_enabled` column when that is non-null, otherwise `display_preferences.sound_enabled !== false` — so an unconfigured screen reports `true` and only an explicit `false` mutes. */
|
|
42
47
|
sound_enabled?: boolean;
|
|
43
48
|
};
|
|
44
49
|
/**
|
|
@@ -50,6 +55,7 @@ type AdSettings = {
|
|
|
50
55
|
*
|
|
51
56
|
*/
|
|
52
57
|
type AdvertisingIdentity = {
|
|
58
|
+
/** DISCARDED whenever `status` is `available` — the server stores `null` in that case, deliberately, because a resettable OS advertising ID stays valid until the OS rotates it and a collector's refresh or cache cadence is not identity expiry. It is only persisted for the non-`available` states. */
|
|
53
59
|
expires_at?: string | null;
|
|
54
60
|
/** Present only when status is `available`. */
|
|
55
61
|
id?: string | null;
|
|
@@ -57,9 +63,13 @@ type AdvertisingIdentity = {
|
|
|
57
63
|
id_type?: "gaid" | "aaid" | "afai" | "rida" | "tifa" | "idfa" | null;
|
|
58
64
|
/** Must be false for `available` and true for `limited`. */
|
|
59
65
|
limit_ad_tracking: boolean | null;
|
|
66
|
+
/** When the device read the identifier from the OS. It is REQUIRED and strictly parsed — an unparseable or missing timestamp causes the entire identity envelope to be dropped without an error response, which is the most common silent rejection on this endpoint. */
|
|
60
67
|
observed_at: string;
|
|
68
|
+
/** Which platform API produced the identifier. `host_supplied` is the value the SERVER itself stamps when it synthesises an envelope from the legacy flat `advertisingId`/`limitAdTracking` fields or from a registration hardware tuple, so seeing it in a read does not mean the device claimed it. An unrecognised source rejects the whole envelope. */
|
|
61
69
|
source: "google_play_services" | "amazon_fire_os" | "roku_os" | "samsung_tizen" | "lg_webos" | "apple_platform" | "host_supplied";
|
|
70
|
+
/** Version of the platform component or SDK that read the identifier (e.g. the Play Services version), up to 128 characters, kept for provenance only. Trimmed, with a whitespace-only value stored as `null`. */
|
|
62
71
|
source_version?: string | null;
|
|
72
|
+
/** The state machine, not a label. `available` is accepted only with a valid `id`, an `id_type` and `limit_ad_tracking: false` — any other combination makes the server DISCARD the whole envelope behind a 200 and keep the previously stored identity. Every other value CLEARS a stored identifier, and `limited` additionally forces `limit_ad_tracking` to true. */
|
|
63
73
|
status: "available" | "limited" | "consent_denied" | "unavailable" | "unsupported" | "error";
|
|
64
74
|
};
|
|
65
75
|
/**
|
|
@@ -67,6 +77,7 @@ type AdvertisingIdentity = {
|
|
|
67
77
|
*
|
|
68
78
|
*/
|
|
69
79
|
type AnalyticsData = {
|
|
80
|
+
/** **Never emitted** by the analytics endpoint, which returns impression counts and cached revenue only. */
|
|
70
81
|
average_cpm: number;
|
|
71
82
|
/** Optional per-screen breakdown. */
|
|
72
83
|
by_screen?: Array<{
|
|
@@ -79,12 +90,17 @@ type AnalyticsData = {
|
|
|
79
90
|
fill_rate: number;
|
|
80
91
|
/** The window this summary covers (e.g. day, week, month). */
|
|
81
92
|
period: string;
|
|
93
|
+
/** **Not emitted under this name.** The analytics endpoint returns `data.partner.active_devices`, derived from the screen's `online_status` via the partner device-status mapping, plus a `devices_by_status` breakdown. */
|
|
82
94
|
screens_active: number;
|
|
95
|
+
/** **Not emitted under this name.** The analytics endpoint returns `data.partner.total_devices`, a count of registered DEVICES rather than of screens — a device with no screen row still counts. */
|
|
83
96
|
screens_total: number;
|
|
97
|
+
/** **Not emitted.** `GET /v1/partner/analytics` returns `data.partner.total_revenue_cents` (integer US cents, read from the `partners.stats` cache) instead; that endpoint has no dollar-denominated earnings field. */
|
|
84
98
|
total_earnings: number;
|
|
99
|
+
/** Declared for the SDK type; `GET /v1/partner/analytics` does not return this shape. It returns `data.partner.total_impressions` — an ALL-TIME deduped `completed_impressions` count that ignores the `start_date`/`end_date` query parameters (those bound only `impressions_by_day`). */
|
|
85
100
|
total_impressions: number;
|
|
86
101
|
};
|
|
87
102
|
type AudienceAnalyticsResponse = {
|
|
103
|
+
/** `overview.total_impressions` counts `event_tracking` `start`/`impression` events — NOT the billable `completed_impressions` the dashboard and earnings endpoints use — and `active_screens` counts screens with ANY event in the window rather than screens that are online. **`trends.impressions_trend` and `trends.audience_trend` are hardcoded to the literal `stable`**, so the `increasing`/`decreasing` values are unreachable. `period` echoes `?period=` (`24h`/`7d`/`30d`; anything else silently means 7 days). */
|
|
88
104
|
data?: {
|
|
89
105
|
audience?: {
|
|
90
106
|
avg_attention?: number;
|
|
@@ -104,7 +120,9 @@ type AudienceAnalyticsResponse = {
|
|
|
104
120
|
impressions_trend?: "increasing" | "stable" | "decreasing";
|
|
105
121
|
};
|
|
106
122
|
};
|
|
123
|
+
/** Per-request identifier (`req_` plus 32 hex), also returned on the `X-Request-Id` response header. */
|
|
107
124
|
request_id?: string;
|
|
125
|
+
/** Always `true` on a 2xx; failures return the error envelope (`success: false` plus an `error` object) at a 4xx/5xx status. */
|
|
108
126
|
success?: boolean;
|
|
109
127
|
};
|
|
110
128
|
/**
|
|
@@ -150,10 +168,13 @@ type AudienceData = {
|
|
|
150
168
|
demographics?: Record<string, unknown>;
|
|
151
169
|
/** Average dwell time for the sample. */
|
|
152
170
|
dwell_time_avg: number;
|
|
171
|
+
/** Declared for the SDK type. `GET /v1/partner/audience/live/{screenId}` puts `screen_id` at the TOP level of an `object: audience_live` envelope alongside `status` and `audience`, not inside this shape. */
|
|
153
172
|
screen_id: string;
|
|
173
|
+
/** **Not emitted under this name.** The live audience endpoint returns `last_updated` — the time of the newest underlying observation, nullable — and returns `audience: null` with `status: offline` when the screen has no live data at all. */
|
|
154
174
|
timestamp: string;
|
|
155
175
|
};
|
|
156
176
|
type AudienceExportResponse = {
|
|
177
|
+
/** One entry per RESOLVED screen (max 50 ids per call), emitted even for screens with no audience data — those get a zeroed `audience` block rather than being omitted. `impressions` counts `event_tracking` `start`/`impression` events, which are NOT the billable `completed_impressions` the dashboard and earnings endpoints use, and every row repeats the same request-level `period`. */
|
|
157
178
|
data?: Array<{
|
|
158
179
|
audience?: {
|
|
159
180
|
avg_attention?: number;
|
|
@@ -172,8 +193,11 @@ type AudienceExportResponse = {
|
|
|
172
193
|
screen_id?: string;
|
|
173
194
|
screen_name?: string;
|
|
174
195
|
}>;
|
|
196
|
+
/** Per-request identifier (`req_` plus 32 hex), also on the `X-Request-Id` header. Absent from the `format=csv` variant, which returns a bare five-column body and no envelope. */
|
|
175
197
|
request_id?: string;
|
|
198
|
+
/** Always `true` — a failure returns the error envelope (`success: false` plus an `error` object) at a 4xx/5xx status, so this field carries nothing the status code does not. */
|
|
176
199
|
success?: boolean;
|
|
200
|
+
/** `total_screens` counts the screens that RESOLVED, so it can be smaller than the id list you sent. `total_impressions` sums the same non-billable `start`/`impression` event counts. `aggregation` echoes the query parameter unvalidated (default `daily`) and does not change the shape of the response. */
|
|
177
201
|
summary?: {
|
|
178
202
|
aggregation?: string;
|
|
179
203
|
period?: Record<string, unknown>;
|
|
@@ -190,10 +214,12 @@ type AudienceExportResponse = {
|
|
|
190
214
|
*
|
|
191
215
|
*/
|
|
192
216
|
type AudienceLoyaltyFeaturesRow = {
|
|
217
|
+
/** How the cross-day device key was formed. `primary_signal_source` is picked by a fixed precedence (`fixed_mac` > `mdns_vendor_model` > `ble_continuity` > `ble_mac_stable`) and `match_confidence` is a direct relabelling of it (first two `high`, `ble_continuity` `medium`, `ble_mac_stable` `low`) — not a computed probability. */
|
|
193
218
|
_match_disclosure?: {
|
|
194
219
|
match_confidence?: "high" | "medium" | "low";
|
|
195
220
|
primary_signal_source?: "ble_continuity" | "ble_mac_stable" | "mdns_vendor_model" | "fixed_mac";
|
|
196
221
|
};
|
|
222
|
+
/** NOT a per-device inference: it is the aggregate audience profile of the first screen in the device's visited screen set that has one, copied onto every device that touched that screen. All six sub-fields are `null` when no visited screen carries a profile. */
|
|
197
223
|
demographic_inference?: {
|
|
198
224
|
age_bracket?: string;
|
|
199
225
|
attention_score_avg?: number;
|
|
@@ -204,12 +230,17 @@ type AudienceLoyaltyFeaturesRow = {
|
|
|
204
230
|
};
|
|
205
231
|
/** Partner-scoped HMAC of cross-day device key (16 hex chars + tb_ prefix). Different partners get different IDs for the same physical device — preventing collusion. */
|
|
206
232
|
device_id_pseudonymous: string;
|
|
233
|
+
/** Distinct calendar days (ClickHouse server timezone) on which this device key was observed. This is the quantity behind the published `returning_visitor` qualification of two or more days. */
|
|
207
234
|
distinct_days_in_window: number;
|
|
235
|
+
/** Distinct SCREENS, not venues — a site with several screens contributes more than one, so this over-counts locations for any multi-screen venue. */
|
|
208
236
|
distinct_venues_in_window?: number;
|
|
209
237
|
/** Days since this device was first observed at any of the partner-scoped screens. */
|
|
210
238
|
first_seen_relative_days: number;
|
|
239
|
+
/** Whole days between now and the newest observation in the window, floored at 0 — `0` means seen today. An unparseable source timestamp also yields `0`, so it cannot be told apart from a same-day sighting. */
|
|
211
240
|
last_seen_relative_days: number;
|
|
241
|
+
/** Tier derived from the Bluetooth-SIG company identifier in the device's advertisement, not from any model string. Only five vendors carry an explicit tier (Apple / Samsung / Google `premium`, Microsoft `mainstream`, Sonos `audio_iot`); every other registered vendor, and every unresolvable id, falls to `other`. */
|
|
212
242
|
manufacturer_class: "premium" | "mainstream" | "audio_iot" | "other";
|
|
243
|
+
/** Vendor name resolved from the same Bluetooth-SIG company id. An id that resolves to no vendor returns the literal `Other tracked devices` rather than `null`, so an unknown device is not distinguishable by a null check. */
|
|
213
244
|
manufacturer_label?: string;
|
|
214
245
|
/** Raw cross-day-stable identifiers when available (NULL for randomized phone MACs without Continuity). */
|
|
215
246
|
raw_identifiers?: {
|
|
@@ -221,13 +252,17 @@ type AudienceLoyaltyFeaturesRow = {
|
|
|
221
252
|
mdns_hostname?: string | null;
|
|
222
253
|
wifi_bssid?: string | null;
|
|
223
254
|
};
|
|
255
|
+
/** Same arbitrary-row behaviour as `top_hour_of_day`, and the value comes from ClickHouse `toDayOfWeek` — **1 = Monday through 7 = Sunday**, which does not fit the declared 0..6 range. */
|
|
224
256
|
top_day_of_week?: number;
|
|
257
|
+
/** Produced by `argMax(toHour(observed_at), 1)`; because the ordering expression is the constant `1` every row ties, so this is the hour of an ARBITRARY observation in the window, not the modal hour. Do not read a peak out of it. */
|
|
225
258
|
top_hour_of_day?: number;
|
|
226
259
|
/** List of venue_category labels (Bar, Restaurant, etc.) the device was observed at. */
|
|
227
260
|
venue_archetypes?: Array<string>;
|
|
261
|
+
/** Count of raw radio OBSERVATIONS, not of visits — one dwelling device emits many rows per minute. It is also the quantity the export's k-anonymity `HAVING` clause tests. */
|
|
228
262
|
visit_count_in_window: number;
|
|
229
263
|
};
|
|
230
264
|
type AudienceMetricsResponse = {
|
|
265
|
+
/** **Currently always the zero/`unknown` fallback.** The handler reads `metrics.live`, `metrics.aggregated` and `metrics.history`, but the underlying `getScreenMetrics` takes a time-range STRING and returns a FLAT object carrying none of those keys — so all three are undefined on every call and the hardcoded zeros are served. `period` echoes `?period=` (default `1h`) and is likewise not honoured downstream; `timestamp` is the response time, not a measurement time. */
|
|
231
266
|
data?: {
|
|
232
267
|
aggregated?: {
|
|
233
268
|
avgAttention?: number;
|
|
@@ -258,24 +293,37 @@ type AudienceMetricsResponse = {
|
|
|
258
293
|
screen_name?: string;
|
|
259
294
|
timestamp?: string;
|
|
260
295
|
};
|
|
296
|
+
/** Per-request identifier (`req_` plus 32 hex), also returned on the `X-Request-Id` response header. */
|
|
261
297
|
request_id?: string;
|
|
298
|
+
/** Always `true` on a 2xx; failures return the error envelope (`success: false` plus an `error` object) at a 4xx/5xx status. */
|
|
262
299
|
success?: boolean;
|
|
263
300
|
};
|
|
264
301
|
type AudienceSegment = {
|
|
302
|
+
/** Creation time of the authoritative PG row; the 201 create response falls back to the server clock only if the insert returned no row. */
|
|
265
303
|
created_at?: string;
|
|
304
|
+
/** Free text, trimmed on write. Defaults to the empty string, never `null`, when omitted at create. */
|
|
266
305
|
description?: string;
|
|
306
|
+
/** 24-character hex ObjectId minted by the API at create time and stored as the PG row's `mongo_id` — not the table's integer primary key. This is the value the get / patch / delete / match routes take as `:segmentId`. */
|
|
267
307
|
id?: string;
|
|
308
|
+
/** Stored and echoed but **nothing reads it** — `POST /segments/{id}/match` runs a segment regardless, and the list endpoint does not filter on it. Treat it as a caller-owned label, not a gate. */
|
|
268
309
|
is_active?: boolean;
|
|
310
|
+
/** When `POST /segments/{id}/match` last ran. The update is fire-and-forget after the response is sent, so a failed write leaves this stale or `null` with no error surfaced to the caller. */
|
|
269
311
|
last_matched_at?: string;
|
|
312
|
+
/** Number of SCREENS returned by the most recent `POST /segments/{id}/match`, capped by that call's `limit` (default 50, max 100) — so it is the size of the last result page, not the segment's reach. `null` until the first match call. */
|
|
270
313
|
last_matched_count?: number;
|
|
314
|
+
/** Trimmed on write, max 100 characters, and unique per partner — a repeat name is rejected `409 duplicate_segment_name` rather than upserting the existing segment. */
|
|
271
315
|
name?: string;
|
|
316
|
+
/** Always the literal `audience_segment`. `GET /audience/segments` wraps these in an `object: list` envelope, so the discriminator appears on both the envelope and each element. */
|
|
272
317
|
object?: string;
|
|
318
|
+
/** Per-request identifier, `req_` plus 32 hex characters, minted server-side and also returned on the `X-Request-Id` response header. A client-supplied `X-Request-Id` is NOT adopted — it is echoed back on `X-Client-Request-Id` instead. */
|
|
273
319
|
request_id?: string;
|
|
320
|
+
/** Predicates combined with AND, at most 20 per segment. `field` is restricted to a 17-name allowlist (`income_level`, `lifestyle_segment`, `attention_score`, `face_count`, `dwell_time_ms`, `crowd_density`, `ad_receptivity`, `emotional_engagement`, `purchase_intent`, `occupancy`, `noise_level`, `venue_type`, `country`, `state`, `city`, `hour_of_day`, `day_of_week`) and `operator` to `eq`/`neq`/`gt`/`gte`/`lt`/`lte`/`in`/`nin`/`contains`; any other key on a rule object is stripped on write. */
|
|
274
321
|
rules?: Array<{
|
|
275
322
|
field?: string;
|
|
276
323
|
operator?: string;
|
|
277
324
|
value?: unknown;
|
|
278
325
|
}>;
|
|
326
|
+
/** Last write to the segment, INCLUDING the fire-and-forget match-stats update — so running a match bumps this even though name, rules and `is_active` did not change. Omitted entirely from the 201 create response. */
|
|
279
327
|
updated_at?: string;
|
|
280
328
|
};
|
|
281
329
|
/**
|
|
@@ -294,11 +342,14 @@ type AudienceSocialAttention = {
|
|
|
294
342
|
type BatchResult = {
|
|
295
343
|
/** Number that failed */
|
|
296
344
|
failed_count?: number;
|
|
345
|
+
/** Not emitted. The live response names this array `errors`, and each entry carries `screen_id`, a human `error` string, and an additional machine `code`: `screen_not_found` for an id that is not 24-hex or that your partner account does not own, `write_failed` for a database failure. Read `errors`. */
|
|
297
346
|
failed_screens?: Array<{
|
|
298
347
|
error?: string;
|
|
299
348
|
screen_id?: string;
|
|
300
349
|
}>;
|
|
350
|
+
/** Constant discriminator `batch_result`, shared verbatim by every partner batch endpoint (content preferences, display settings, programmatic settings) — it does not identify which one you called. */
|
|
301
351
|
object?: string;
|
|
352
|
+
/** Server-minted per HTTP request in the form `req_<32 hex>`, also returned in the `X-Request-Id` response header; omitted if no request id was assigned to the call. Quote it when reporting a partial batch failure — it is the only handle that ties your call to the per-screen error rows. */
|
|
302
353
|
request_id?: string;
|
|
303
354
|
/** Number of screens requested to update */
|
|
304
355
|
total_requested?: number;
|
|
@@ -313,8 +364,11 @@ type BatchResult = {
|
|
|
313
364
|
*
|
|
314
365
|
*/
|
|
315
366
|
type BillingInfo = {
|
|
367
|
+
/** **Never emitted**, so its unit is undefined. The credits that do exist are bought through `POST /billing/credits`, which reports `credits_added_cents` — integer US cents, not an abstract credit count. */
|
|
316
368
|
credits_remaining: number;
|
|
369
|
+
/** **Never emitted.** Consumption is reported by `GET /billing/usage` (`object: billing_usage`), which returns the billing service's usage summary rather than a credit counter. */
|
|
317
370
|
credits_used: number;
|
|
371
|
+
/** **No producer.** There is no `GET /v1/partner/billing` route; the live billing surface is `/billing/plans`, `/billing/subscribe`, `/billing/cancel`, `/billing/usage` and `/billing/subscription`, each returning a Stripe-style envelope tagged with an `object` marker plus `request_id`. */
|
|
318
372
|
plan: string;
|
|
319
373
|
/** Whether the partner has a connected Stripe account. */
|
|
320
374
|
stripe_connected: boolean;
|
|
@@ -322,16 +376,27 @@ type BillingInfo = {
|
|
|
322
376
|
usage_this_month: Record<string, number>;
|
|
323
377
|
};
|
|
324
378
|
type BleScanResultData = {
|
|
379
|
+
/** Despite the name this is a GENERALISED advert-subtype code, and the server stores it as `manufacturer_subtype`. Values 0x00-0x1F are genuine Apple Continuity subtypes; 0x80 and above are SYNTHETIC markers this parser assigns to non-Apple formats — 0x80 iBeacon, 0x81 Eddystone-UID, 0x82 Fast Pair, 0x83 Tile, 0x85 Eddystone-URL, 0x86 AltBeacon. Note an Apple iBeacon reports 0x80, not its literal Apple subtype 0x02. */
|
|
325
380
|
appleContinuitySubtype?: number | null;
|
|
381
|
+
/** The Android `BluetoothDevice.getType()` constant: 0 UNKNOWN, 1 CLASSIC, 2 LE, 3 DUAL. Treat `0` as "unknown OR the read threw" — the producer catches any exception and substitutes 0 rather than omitting the field. */
|
|
326
382
|
deviceType?: number | null;
|
|
383
|
+
/** iBeacon major value, an unsigned 16-bit integer (0-65535) parsed BIG-endian from the advert. `null` unless a valid iBeacon frame was found, and meaningless without the accompanying `ibeaconUuid`. */
|
|
327
384
|
ibeaconMajor?: number | null;
|
|
385
|
+
/** iBeacon minor value, unsigned 16-bit big-endian (0-65535). `null` outside a valid iBeacon frame; major and minor are only comparable within one `ibeaconUuid` namespace. */
|
|
328
386
|
ibeaconMinor?: number | null;
|
|
387
|
+
/** The 16-byte iBeacon proximity UUID as canonical dashed lowercase, parsed BIG-endian. Present only for a well-formed Apple iBeacon frame — an Apple subtype 0x02 whose inner length is not 0x15 is explicitly rejected rather than partially parsed. */
|
|
329
388
|
ibeaconUuid?: string | null;
|
|
389
|
+
/** The Bluetooth SIG assigned company identifier from the advert's Manufacturer Specific Data structure (AD type 0xFF), read little-endian — e.g. 76 (0x004c) Apple, 6 Microsoft, 224 (0x00e0) Google. `null` when the record carries no such structure. One synthetic exception: a Google Fast Pair service-data advert is forced to the Google id even though it has no 0xFF structure. */
|
|
330
390
|
manufacturerCompanyId?: number | null;
|
|
391
|
+
/** The BLE advertiser address exactly as Android reports it, unhashed on the device — it is sent over TLS and HMAC-hashed server-side. On most modern phones this is a resolvable private address that ROTATES roughly every 15 minutes, so it is not a stable device identity; the sibling `addr_type` (`public`/`random`/`anonymous`) is what tells the two apart. */
|
|
331
392
|
rawAddress: string;
|
|
393
|
+
/** Received signal strength in dBm (negative, bounded -120..0). It is the MAXIMUM RSSI across every advertisement from this address inside the scan window — the closest observed reading, not a mean — because the scanner groups repeated adverts per address and keeps the strongest as the representative. */
|
|
332
394
|
rssi: number;
|
|
395
|
+
/** Advertised GATT service UUIDs, de-duplicated. 16-bit UUIDs are rendered as FOUR lowercase hex characters (e.g. `feaa`), not expanded to the full `0000feaa-0000-1000-8000-00805f9b34fb` base form; 128-bit UUIDs are canonical dashed lowercase. `null` rather than `[]` when the advert carries none. */
|
|
333
396
|
serviceUuids?: Array<string> | null;
|
|
397
|
+
/** Lowercase hex, no separators, of ONLY the advert sub-bytes that survive address rotation (an iBeacon UUID+major+minor, an Eddystone namespace+instance, a Fast Pair model id…), prefixed with the synthetic subtype byte. Calibration bytes such as TX power are deliberately excluded. It is `null` far more often than it is present: 13 rotating Apple subtypes are dropped outright and unknown Apple subtypes fail closed, so a populated `appleContinuitySubtype` with a null payload here is normal. */
|
|
334
398
|
stableManufacturerPayloadHex?: string | null;
|
|
399
|
+
/** Advertised transmit power in dBm (bounded -127..20) taken from the advert itself, NOT from `ScanResult.getTxPower()`. For iBeacon and Eddystone frames it is the format's calibrated 1-metre reference power, which overwrites the generic AD-type-0x0A value; `null` when the advert carries neither. */
|
|
335
400
|
txPowerDbm?: number | null;
|
|
336
401
|
[key: string]: unknown;
|
|
337
402
|
};
|
|
@@ -466,14 +531,23 @@ type ContentPreferencesInput = {
|
|
|
466
531
|
preferred_categories?: Array<string>;
|
|
467
532
|
};
|
|
468
533
|
type ContentPreferencesResponse = {
|
|
534
|
+
/** NOT EMITTED. No handler on this endpoint produces this key — neither `formatContentPreferences` nor the service feeding it has such a field. Advertiser blocking lives on the programmatic-settings resource instead, as `blocked_advertisers` on `GET/PUT /v1/partner/screens/{screenId}/programmatic-settings`. */
|
|
469
535
|
blocked_advertisers?: Array<string>;
|
|
536
|
+
/** The screen's own blocked list, `[]` when unset. Matched at approval time against the ad's typed `iab_categories` with `categoriesMatch`, so IAB codes and human-readable names both work. It is the only one of this schema's five list/rating fields the server actually emits. */
|
|
470
537
|
blocked_categories?: Array<string>;
|
|
538
|
+
/** NOT EMITTED, and the identifier does not occur anywhere in the API source. The only delivery cap this platform exposes to partners is the advisory `max_ads_per_hour` in the `/ads` response settings. */
|
|
471
539
|
max_ad_frequency?: number;
|
|
540
|
+
/** NOT EMITTED, and the identifier `min_ad_rating` does not occur anywhere in the API source. The response carries `max_age_rating` instead — an UPPER bound on the ad classification (`G`..`NC-17`), defaulting to `PG-13`, not a minimum. */
|
|
472
541
|
min_ad_rating?: string;
|
|
542
|
+
/** Always the literal `content_preferences`. This response is FLAT — there is no `success`/`data` envelope, unlike the L-bar and default-stream routes — and `POST .../content-preferences/apply-preset` returns this same object with an extra `preset_applied` field appended. */
|
|
473
543
|
object?: string;
|
|
544
|
+
/** NOT EMITTED, and the identifier does not occur anywhere in the API source. The nearest real control is `allowed_categories`, which the formatter does return and which acts as a scoring boost (+10 per match) rather than a hard preference. */
|
|
474
545
|
preferred_categories?: Array<string>;
|
|
546
|
+
/** Echo of the server-assigned `req.requestId`. The formatter attaches it only when one exists, so the key is ABSENT rather than null on a request that carried none. */
|
|
475
547
|
request_id?: string;
|
|
548
|
+
/** Echoed straight back from the `{screenId}` path parameter — the formatter is handed the path value, not a column — so it always equals what you asked for. */
|
|
476
549
|
screen_id?: string;
|
|
550
|
+
/** ISO-8601, but it is the SCREEN row's `updatedAt`, not a preferences-specific timestamp, so unrelated screen edits move it. When the screen has none the formatter substitutes the current time, so this field is never null and never proves a write landed. */
|
|
477
551
|
updated_at?: string;
|
|
478
552
|
};
|
|
479
553
|
type ContentPreset = {
|
|
@@ -493,12 +567,18 @@ type ContentPreset = {
|
|
|
493
567
|
preferences?: Record<string, unknown>;
|
|
494
568
|
};
|
|
495
569
|
type CreatePartnerSensingRouteRequest = {
|
|
570
|
+
/** The channel actions fired on a match; at least 1 and at most 10, each carrying exactly the destination its channel requires. A route with zero actions cannot exist, so use a `webhook` action for anything you do not want delivered to a person. */
|
|
496
571
|
actions: Array<SensingRouteAction>;
|
|
572
|
+
/** Seven-bit weekday mask evaluated in `activeTz`: bit 0 Monday through bit 6 Sunday, so the default 127 is every day and 0 means the route never fires. Requires `activeTz`. */
|
|
497
573
|
activeDaysMask?: number;
|
|
574
|
+
/** Inclusive end of the daily active window, minutes past local midnight in `activeTz` (0..1439). Set it BELOW `activeStartMinute` for an overnight window — 1200 to 360 is 20:00 through 06:00. */
|
|
498
575
|
activeEndMinute?: number;
|
|
576
|
+
/** Inclusive start of the daily active window, minutes past local midnight in `activeTz` (0..1439; 1200 = 20:00). Has no effect without `activeTz`, and omitting either end makes the window whole-day. */
|
|
499
577
|
activeStartMinute?: number;
|
|
578
|
+
/** IANA zone name (e.g. `America/New_York`) the active window is evaluated in, trimmed to 64 characters. Omitting it makes `activeStartMinute`, `activeEndMinute` and `activeDaysMask` inert — the route is then always active — and an unparseable zone fails open. */
|
|
500
579
|
activeTz?: string;
|
|
501
580
|
condition?: SensingRouteCondition;
|
|
581
|
+
/** Minimum gap in seconds between alerts for the same (route, screen) episode; clamped to 0..86400, default 300. It also bounds the falling-edge clear window at 60 s, so a value below that re-arms the route faster too. */
|
|
502
582
|
debounceSeconds?: number;
|
|
503
583
|
/** Required when mode=digest. `field` is required for avg/max. */
|
|
504
584
|
digest?: {
|
|
@@ -507,8 +587,11 @@ type CreatePartnerSensingRouteRequest = {
|
|
|
507
587
|
metric?: "count" | "avg" | "max";
|
|
508
588
|
windowMinutes?: number;
|
|
509
589
|
};
|
|
590
|
+
/** Only the JSON literal `false` creates the route disabled — any other value, including `0` and the string `"false"`, creates it enabled. Note the update endpoint inverts this test, so send a real boolean on both. */
|
|
510
591
|
enabled?: boolean;
|
|
592
|
+
/** `threshold` (the default) needs a `condition` and evaluates every incoming observation. `digest` needs a `digest` block instead, sends on a schedule, and rejects `screenFocusRegions`. Only the exact string `digest` selects digest mode; anything else falls back to `threshold`. */
|
|
511
593
|
mode?: "threshold" | "digest";
|
|
594
|
+
/** Operator-supplied label, required, trimmed and capped at 200 characters. It is delivered as `route_name` on every fire and shown in operator surfaces; it is not the alert text. */
|
|
512
595
|
name: string;
|
|
513
596
|
/** Bind to a named Sense Anything profile (stable across redeploys). */
|
|
514
597
|
profileName?: string;
|
|
@@ -529,9 +612,11 @@ type CreatePartnerSensingRouteRequest = {
|
|
|
529
612
|
* Structured creative classification — the same shape the Trillboards venue portal renders for a served ad.
|
|
530
613
|
*/
|
|
531
614
|
type CreativeClassification = {
|
|
615
|
+
/** The name whoever FIRST submitted these bytes passed on `POST /creatives`, carried through the moderation queue. It is unverified free text, and because the review row is keyed on the content hash a later submitter of the same bytes reads the first submitter's value. `null` when none was supplied. */
|
|
532
616
|
advertiser_name?: string | null;
|
|
533
617
|
/** One of G, PG, PG-13, R, NC-17 (null until classified). */
|
|
534
618
|
age_rating?: string | null;
|
|
619
|
+
/** Descriptive output of the same classifier pass — visible subjects, keywords, scene setting, mood, a quality grade, and `text_content` recovered by OCR. Every field is `null`/empty until classification finishes. This is scene description, not the moderation verdict; that is `status` plus `moderation.flags`. */
|
|
535
620
|
analysis?: {
|
|
536
621
|
keywords?: Array<string>;
|
|
537
622
|
mood?: string | null;
|
|
@@ -541,6 +626,7 @@ type CreativeClassification = {
|
|
|
541
626
|
/** On-screen text (OCR). */
|
|
542
627
|
text_content?: string | null;
|
|
543
628
|
};
|
|
629
|
+
/** Classifier output: `brand`, `category` and IAB category codes. All three are `null`/empty while `status` is `pending`, so an empty `iab_categories` means not yet classified rather than 'no categories apply'. */
|
|
544
630
|
categorization?: {
|
|
545
631
|
brand?: string | null;
|
|
546
632
|
category?: string | null;
|
|
@@ -548,18 +634,24 @@ type CreativeClassification = {
|
|
|
548
634
|
};
|
|
549
635
|
/** Classification confidence, 0..1. */
|
|
550
636
|
confidence?: number | null;
|
|
637
|
+
/** SHA-256 of the bytes the SERVER fetched from your `media_url` — lowercase 64-hex, never a hash you declare — and `id` is this same value prefixed `sha256:`. Classification is content-addressed and global, so two partners submitting identical bytes share one record, and this is the byte-identity join key into `completed_impressions` and `vast_requests`. */
|
|
551
638
|
content_sha256?: string;
|
|
552
639
|
/** Canonical reference — `sha256:<hex>` of the creative bytes. Use as `creative_ref` when posting plays. */
|
|
553
640
|
id?: string;
|
|
641
|
+
/** `approved` is not an independent verdict — it is derived as `status === 'approved'`, so it is false for both `pending` and `blocked` and adds no information beyond `status`. `flags` is the classifier's reason list and is empty until classification finishes. */
|
|
554
642
|
moderation?: {
|
|
555
643
|
approved?: boolean;
|
|
556
644
|
flags?: Array<string>;
|
|
557
645
|
};
|
|
646
|
+
/** Constant discriminator `creative_classification`. It is returned identically on the 202 (queued) and 200 (already classified) forms of this response, so it tells you nothing about whether classification has finished — read `status` for that. */
|
|
558
647
|
object?: string;
|
|
648
|
+
/** `pending` means queued and not yet classified. `approved`, `flagged` and `blocked` are terminal: re-submitting the same bytes then returns 200 immediately without re-running the classifier or consuming your daily classification quota, whereas a still-pending submit returns 202. The value is passed through from the shared review row unmapped, so a state outside this enum (e.g. `metadata_only`) can surface. */
|
|
559
649
|
status?: "pending" | "approved" | "flagged" | "blocked";
|
|
650
|
+
/** Always `null` in practice: the only writer of the underlying `creative_reviews.thumbnail_url` column has no remaining callers, so nothing populates it. When it did carry a value it was constrained to a Trillboards CDN host, so an off-host thumbnail was dropped rather than passed through. */
|
|
560
651
|
thumbnail_url?: string | null;
|
|
561
652
|
};
|
|
562
653
|
type DashboardResponse = {
|
|
654
|
+
/** All amounts are integer US cents. `total_earned_cents` is lifetime earner revenue across every status (estimated + confirmed + paid); `available_payout_cents` is only the Net-60-matured payable balance minus anything already reserved, floored at 0; `next_payout_date` is simply the 1st of next month, computed in the handler with no reference to a scheduled transfer. `revenue_share_percent` is 0-100 and reads `60` when no `publisher_payout_rates` rule applies. */
|
|
563
655
|
earnings?: {
|
|
564
656
|
available_payout_cents?: number;
|
|
565
657
|
next_payout_date?: string;
|
|
@@ -567,18 +659,21 @@ type DashboardResponse = {
|
|
|
567
659
|
revenue_share_percent?: number;
|
|
568
660
|
total_earned_cents?: number;
|
|
569
661
|
};
|
|
662
|
+
/** Identity block copied off the partner row, where `created_at` is the registration timestamp. The row is read through a 10-minute Redis cache, so a just-changed name or status can lag by that much. */
|
|
570
663
|
partner?: {
|
|
571
664
|
created_at?: string;
|
|
572
665
|
name?: string;
|
|
573
666
|
slug?: string;
|
|
574
667
|
status?: "pending" | "active" | "suspended" | "terminated";
|
|
575
668
|
};
|
|
669
|
+
/** The ten newest `impression_ledger` rows for the partner, newest first. `type` is always the literal `impression` — no other activity kind is ever emitted. */
|
|
576
670
|
recent_activity?: Array<{
|
|
577
671
|
ad_id?: string;
|
|
578
672
|
screen_id?: string;
|
|
579
673
|
timestamp?: string;
|
|
580
674
|
type?: string;
|
|
581
675
|
}>;
|
|
676
|
+
/** `active_screens`/`offline_screens` count `online_status` `online`/`offline` from the live heartbeat mirror — a screen that has never heartbeated reads `unknown` and lands in NEITHER bucket, so the two need not sum to `total_screens`. `pending_screens` is the separate approval status and overlaps both. The three impression totals are deduped, GIVT-passed `completed_impressions` over midnight-today / the last 7 days / month-to-date, all cut in the API server's local timezone rather than the screen's. */
|
|
582
677
|
stats?: {
|
|
583
678
|
active_screens?: number;
|
|
584
679
|
offline_screens?: number;
|
|
@@ -588,10 +683,12 @@ type DashboardResponse = {
|
|
|
588
683
|
total_impressions_week?: number;
|
|
589
684
|
total_screens?: number;
|
|
590
685
|
};
|
|
686
|
+
/** `connect_account_id` is **redacted**: it is the literal string `***` when an account is connected and `null` when it is not, so it can never be used as an identifier. `account_status` defaults to `pending` and must be `active` before a payout request is accepted. */
|
|
591
687
|
stripe_status?: {
|
|
592
688
|
account_status?: string;
|
|
593
689
|
connect_account_id?: string;
|
|
594
690
|
};
|
|
691
|
+
/** The five screens with the most completed impressions since midnight today (server timezone). `name` falls back to the literal `Unknown Screen`, and `location` is a `City, State` string or `null` when either part is missing. */
|
|
595
692
|
top_screens?: Array<{
|
|
596
693
|
impressions_today?: number;
|
|
597
694
|
location?: string;
|
|
@@ -605,6 +702,7 @@ type DashboardResponse = {
|
|
|
605
702
|
*
|
|
606
703
|
*/
|
|
607
704
|
type DataExportSummaryResponse = {
|
|
705
|
+
/** `lookback_days` is `min(?days, 90)` and defaults to 7. `screen_count` is how many screens the export is SCOPED to, not how many hold data. **`k_anonymity_threshold` is the constant `0`** — the dataset exports apply no suppression despite the field name; the `>= 5` floor quoted in the published methodology applies only to the cross-network proximity/household exports. Each `datasets` entry is a `total_records`/`earliest`/`latest` triple, and a dataset whose count query FAILED is reported as `0` with null bounds rather than as an error. */
|
|
608
706
|
data?: {
|
|
609
707
|
/** Map of dataset name to record summary */
|
|
610
708
|
datasets?: Record<string, {
|
|
@@ -616,6 +714,7 @@ type DataExportSummaryResponse = {
|
|
|
616
714
|
lookback_days?: number;
|
|
617
715
|
screen_count?: number;
|
|
618
716
|
};
|
|
717
|
+
/** Always `true`; a failure is raised to the global error handler and returns a canonical error envelope at a 4xx/5xx status. Note this response carries **no `request_id`**, unlike the sibling audience endpoints. */
|
|
619
718
|
success?: boolean;
|
|
620
719
|
};
|
|
621
720
|
/**
|
|
@@ -634,6 +733,7 @@ type DemandPartners = {
|
|
|
634
733
|
updated_at: string;
|
|
635
734
|
};
|
|
636
735
|
type DemographicsResponse = {
|
|
736
|
+
/** `demographics.age_distribution` and `gender_split` are **hardcoded zeros even on the success path** — nothing computes them. `income_profile` and `group_composition` are shares in 0-1, not percentages, and `lifestyle_segments` is emitted as `{segment, percentage}` objects rather than the plain strings declared here. `data_quality.confidence` is a step function of sample size (>=100 gives 0.9, >=50 gives 0.7, >=20 gives 0.5, >=5 gives 0.3, else 0.1), not a statistical confidence, and `sample_size` counts measurement WINDOWS, not people. `period` echoes `?period=`; an unrecognised value silently falls back to 7 days while still being echoed verbatim. */
|
|
637
737
|
data?: {
|
|
638
738
|
data_quality?: {
|
|
639
739
|
confidence?: number;
|
|
@@ -670,12 +770,17 @@ type DemographicsResponse = {
|
|
|
670
770
|
screen_name?: string;
|
|
671
771
|
timestamp?: string;
|
|
672
772
|
};
|
|
773
|
+
/** Per-request identifier (`req_` plus 32 hex), also returned on the `X-Request-Id` response header. */
|
|
673
774
|
request_id?: string;
|
|
775
|
+
/** Always `true` on a 2xx; failures return the error envelope (`success: false` plus an `error` object) at a 4xx/5xx status. */
|
|
674
776
|
success?: boolean;
|
|
675
777
|
};
|
|
676
778
|
type Device = {
|
|
779
|
+
/** Creation time of the DEVICE row (`earner_screen_devices`), not of the linked screen, which may be created later in the same request or on a subsequent re-registration. */
|
|
677
780
|
created_at?: string;
|
|
781
|
+
/** Trillboards-minted 24-char Mongo ObjectId hex for the `earner_screen_devices` row (`mongo_earnerscreen_device_id`). This is NOT the `device_id` you sent at registration — that comes back as `external_device_id`; the batch endpoint calls this same value `trillboard_id`. All three of `device_id`, `fingerprint` and `external_device_id` are accepted as the `{deviceId}` path parameter, resolved in that order. */
|
|
678
782
|
device_id?: string;
|
|
783
|
+
/** Round-tripped through the OpenRTB `devicetype` integer, so it is LOSSY and will often not equal what you registered: `tv`, `connected_tv`, `display` and any unrecognised value all collapse to `digital_signage`, and `vending_machine` collapses to `kiosk`. Only `mobile`, `tablet`, `digital_signage` and `kiosk` are reachable from a partner registration; `other` appears only for a device whose `devicetype` was written by a non-partner path. */
|
|
679
784
|
device_type?: string;
|
|
680
785
|
/** Screen display specification. `width`/`height` are pixel resolution; `width_in`/`height_in` are physical inches. Pixel-only inputs are NOT recorded as physical size — pass `width_in`/`height_in` (most accurate) or `ppi` so the server can derive inches from pixels. */
|
|
681
786
|
display?: {
|
|
@@ -691,20 +796,33 @@ type Device = {
|
|
|
691
796
|
/** Physical screen width in inches */
|
|
692
797
|
width_in?: number;
|
|
693
798
|
};
|
|
799
|
+
/** Static player URL of the form `https://screen.trillboards.com?fp=<fingerprint>`. It is deterministic and permanent — not a signed or expiring link — so it never rotates and anyone holding it can load the player. `null` until a screen is linked. */
|
|
694
800
|
embed_url?: string;
|
|
801
|
+
/** Venue-enrichment state. `status` is `complete` only when a classification source exists AND `location` actually resolved (coordinates or a city), so it never reports complete over placeholder geo; a partner-supplied venue category stamps the source as `partner` at registration, which means `complete` does not imply AI classification ran. `confidence` is dead — `venue_classification_confidence` has no column and no writer anywhere in the API, so it is always undefined and omitted from the JSON. */
|
|
695
802
|
enrichment?: Record<string, unknown>;
|
|
803
|
+
/** Verbatim echo of the `device_id` you sent in the registration body (`earner_screen_devices.partner_external_id`). It is the idempotency key scoped to your partner account: re-POSTing the same value updates the existing device instead of creating a second one. */
|
|
696
804
|
external_device_id?: string;
|
|
805
|
+
/** Deterministic device key: `P_` followed by the first 24 hex chars of `md5("<your partner id>_<external_device_id>")`. It never rotates — the same partner plus the same `device_id` always regenerates it — and it is both the heartbeat/player routing key and the token embedded in `embed_url`. */
|
|
697
806
|
fingerprint?: string;
|
|
807
|
+
/** Resolved geo, read from the enriched screen row first and falling back to the raw registration-time device row, with placeholder strings ("City", "Vending Machine Location") filtered out. `lat`/`lng` coalesce as a PAIR — a half-set or null-island (0,0) screen pair is discarded whole in favour of the device's coordinates — and `venue_type` here is the OpenOOH CHILD slug, while `venue.category` carries the parent. `timezone` is the one exception to screen-first: it is read device-first. */
|
|
698
808
|
location?: Record<string, unknown>;
|
|
809
|
+
/** The linked screen's name, falling back to the raw `external_device_id` when the screen has none. Omitting `name` at registration seeds the screen as `Partner Device: <device_id>`, so that literal string is what you get back rather than a null. */
|
|
699
810
|
name?: string;
|
|
811
|
+
/** The linked screen's Mongo ObjectId hex (`earner_screens.mongo_earnerscreen_id`), minted server-side at registration. This — not `device_id` — is the id the non-device partner endpoints take (display settings, screen groups, fleet commands, analytics). Absent when no screen is linked to the device. */
|
|
700
812
|
screen_id?: string;
|
|
701
|
-
|
|
813
|
+
/** Provisioning state of the underlying screen row, not a liveness signal — a device that has stopped reporting still reads `approved`. Partner registration stamps `approved` on create (`partnerDeviceRegistrationService.js`), which is what almost every live partner device returns; `pending` is the formatter's fallback when the screen row carries no status of its own. */
|
|
814
|
+
status?: "pending" | "approved" | "active" | "offline" | "suspended";
|
|
815
|
+
/** `earner_screen_devices.updated_at`, which the device-mirror write buffer stamps `NOW()` on every flush — including heartbeat telemetry. It therefore tracks device liveness, not configuration changes; do not poll it to detect that a registration or PATCH landed. */
|
|
702
816
|
updated_at?: string;
|
|
817
|
+
/** OpenOOH classification of the screen, rebuilt from the typed `screen_venue_taxonomy` row. `category` is the PARENT slug and `subcategory` the child, with `openooh_venue_type_id` the unambiguous numeric child id — so a granular input like `hotel` reads back as a parent/child pair, not as what you sent. Empty until classification lands, unless your registration itself carried a resolvable `venue.category`/`subcategory`. */
|
|
703
818
|
venue?: Record<string, unknown>;
|
|
704
819
|
};
|
|
705
820
|
type DeviceCapabilityPayload = {
|
|
821
|
+
/** Runtime family of the agent (`fire-tv`, `android-tv`, `tablet`, `tablet-lite`, `webos`, `vidaa`, `web-browser`, `brightsign`). It is NOT read from here on the partner path — the server derives `agent_type` from `telemetry.agentType` plus hardware signals, and overrides a self-declared `web-browser` when the model/UA say otherwise, because partner SDKs running in a WebView historically reported `web-browser` for physical DOOH tablets. */
|
|
706
822
|
agentType?: string | null;
|
|
823
|
+
/** Agent build version. Do not use it to identify an artifact: three different Android build constants populate this one field across three unrelated numbering schemes, so the same string can mean different code. It is also not read from here on the partner path — the version the server keeps comes from the body `agent_version`/`sdk_version`, and only for heartbeats that self-identify as Trillboards SDKs. */
|
|
707
824
|
agentVersion?: string | null;
|
|
825
|
+
/** Camera/microphone sensing state. On the partner path ONLY the nested `cameraHealth` block is persisted (to the typed `camera_health_*` columns) — `cameraAvailable`, `cameraType`, `cameraCount`, `microphoneAvailable` and `sensingMode` reach no column on this path. The presence of `cameraHealth` is also an in-band signal that the heartbeat came from a Trillboards SDK, which is what allows `agent_version` to be stored. `cameraAvailable` self-devolves to false after a camera has been failed for over 24 h. */
|
|
708
826
|
audienceSensing?: {
|
|
709
827
|
cameraAvailable?: boolean | null;
|
|
710
828
|
cameraCount?: number | null;
|
|
@@ -721,23 +839,29 @@ type DeviceCapabilityPayload = {
|
|
|
721
839
|
sensingMode?: string | null;
|
|
722
840
|
[key: string]: unknown;
|
|
723
841
|
} | null;
|
|
842
|
+
/** Declares volume and mute control. Hardcoded `true`/`true` on the Android agents — it is not probed against `AudioManager`, so it is a claim about the agent, not proof the hardware obeys. Parsed by the shared capability builder but then DISCARDED on the partner heartbeat path: it is not in the partner column whitelist, which is a deliberate security boundary — partner SDKs must not be able to drive device management or power. */
|
|
724
843
|
audioControl?: {
|
|
725
844
|
mute?: boolean | null;
|
|
726
845
|
volume?: boolean | null;
|
|
727
846
|
[key: string]: unknown;
|
|
728
847
|
} | null;
|
|
848
|
+
/** Schema version of this capability block, persisted to `earner_screen_devices.capabilities_version` for drift attribution. Every shipped Android and Vega agent emits the constant `1` and nothing overrides it, so it carries no information today — but the server treats it as an arbitrary integer, not a bounded enum. Note the server only records it when an `mlCapabilities` block is present or when it is sent with no `mlCapabilities` at all. */
|
|
729
849
|
capabilitiesVersion?: number | null;
|
|
850
|
+
/** Declares an ambient-light and a temperature sensor. Only `ambientLight` is a real probe (`SensorManager.getDefaultSensor(TYPE_LIGHT) != null`, false on any throw); `temperature` is hardcoded `false` on the Android agents, and the actual temperature reading that does ship comes from the battery sensor via telemetry, not from here. Parsed by the shared capability builder but then DISCARDED on the partner heartbeat path: it is not in the partner column whitelist, which is a deliberate security boundary — partner SDKs must not be able to drive device management or power. */
|
|
730
851
|
environmentSensors?: {
|
|
731
852
|
ambientLight?: boolean | null;
|
|
732
853
|
temperature?: boolean | null;
|
|
733
854
|
[key: string]: unknown;
|
|
734
855
|
} | null;
|
|
856
|
+
/** Declares HDMI input switching and app switching. Also hardcoded per agent build rather than probed (a tablet reports `hdmi: false`, `appSwitch: true`). Parsed by the shared capability builder but then DISCARDED on the partner heartbeat path: it is not in the partner column whitelist, which is a deliberate security boundary — partner SDKs must not be able to drive device management or power. */
|
|
735
857
|
inputControl?: {
|
|
736
858
|
appSwitch?: boolean | null;
|
|
737
859
|
hdmi?: boolean | null;
|
|
738
860
|
[key: string]: unknown;
|
|
739
861
|
} | null;
|
|
862
|
+
/** Whether the agent holds Android Device Owner (`DevicePolicyManager.isDeviceOwnerApp`) — the privilege that gates reboot, kiosk lock, APK install and wipe. Parsed by the shared capability builder but then DISCARDED on the partner heartbeat path: it is not in the partner column whitelist, which is a deliberate security boundary — partner SDKs must not be able to drive device management or power, precisely so a partner cannot flip it and unlock those commands. */
|
|
740
863
|
isDeviceOwner?: boolean | null;
|
|
864
|
+
/** Declares remote restart/reboot/screenshot/cache-clear/kiosk/APK-install/wipe. On the first-party Android agents four of the seven are simply `isDeviceOwner` repeated, and the partner SDK reports the whole block `false` because "partner hosts own their device management". Parsed by the shared capability builder but then DISCARDED on the partner heartbeat path: it is not in the partner column whitelist, which is a deliberate security boundary — partner SDKs must not be able to drive device management or power. */
|
|
741
865
|
managementCapabilities?: {
|
|
742
866
|
clearCache?: boolean | null;
|
|
743
867
|
installApk?: boolean | null;
|
|
@@ -748,6 +872,7 @@ type DeviceCapabilityPayload = {
|
|
|
748
872
|
wipe?: boolean | null;
|
|
749
873
|
[key: string]: unknown;
|
|
750
874
|
} | null;
|
|
875
|
+
/** The hardware manifest that decides which on-device ML models a screen is offered, and one of the few capability blocks the partner path DOES persist. Two traps: every nullable integer must be sent as JSON `null`, never 0 — the server treats 0 as a probe-failure sentinel and drops it so the COALESCE upsert preserves the prior value — and `screenWidthPx`/`screenHeightPx` below 100 are rejected outright, after agents reporting inches corrupted 940+ rows. A `recommendedModelTier` of `UNKNOWN` (or NULL) excludes the device from on-device demographics entirely. */
|
|
751
876
|
mlCapabilities?: {
|
|
752
877
|
availableRamMb?: number | null;
|
|
753
878
|
chipsetName?: string | null;
|
|
@@ -767,12 +892,14 @@ type DeviceCapabilityPayload = {
|
|
|
767
892
|
totalRamMb?: number | null;
|
|
768
893
|
[key: string]: unknown;
|
|
769
894
|
} | null;
|
|
895
|
+
/** Declares HDMI-CEC, wake-on-LAN and soft-blackout support. On the Android agents these are HARDCODED per build, not probed (a tablet always reports `cec: false`, `wol: false`, `softBlackout: true`), so they describe the agent variant rather than the panel. Parsed by the shared capability builder but then DISCARDED on the partner heartbeat path: it is not in the partner column whitelist, which is a deliberate security boundary — partner SDKs must not be able to drive device management or power. */
|
|
770
896
|
powerControl?: {
|
|
771
897
|
cec?: boolean | null;
|
|
772
898
|
softBlackout?: boolean | null;
|
|
773
899
|
wol?: boolean | null;
|
|
774
900
|
[key: string]: unknown;
|
|
775
901
|
} | null;
|
|
902
|
+
/** Counts ESP32-S3 Wi-Fi CSI sensor nodes discovered on the LAN — it is a statement about the venue's network, not about this device's own radio (`csiHardwareType` is the literal `"esp32-s3"` when any node is found, `"none"` otherwise). Parsed by the shared capability builder but then DISCARDED on the partner heartbeat path: it is not in the partner column whitelist, which is a deliberate security boundary — partner SDKs must not be able to drive device management or power; it is also the one CSI field the ingest schema deliberately keeps, since it is a startup capability counter rather than a streaming data field. */
|
|
776
903
|
wifiCsiSensing?: {
|
|
777
904
|
csiAvailable?: boolean | null;
|
|
778
905
|
csiHardwareType?: string | null;
|
|
@@ -782,19 +909,33 @@ type DeviceCapabilityPayload = {
|
|
|
782
909
|
[key: string]: unknown;
|
|
783
910
|
};
|
|
784
911
|
type DeviceSummary = {
|
|
912
|
+
/** Creation time of the DEVICE row, not of the linked screen (which can be created later). */
|
|
785
913
|
created_at?: string;
|
|
914
|
+
/** Trillboards-minted 24-char Mongo ObjectId hex for the `earner_screen_devices` row (`mongo_earnerscreen_device_id`). Not the `device_id` you sent at registration — that is returned as `external_device_id`. Accepted as the `{deviceId}` path parameter, as are `fingerprint` and `external_device_id`. */
|
|
786
915
|
device_id?: string;
|
|
916
|
+
/** Round-tripped through the OpenRTB `devicetype` integer and therefore lossy: `tv`, `connected_tv`, `display` and unrecognised values all read back as `digital_signage`, and `vending_machine` reads back as `kiosk`. */
|
|
787
917
|
device_type?: string;
|
|
918
|
+
/** Emits `orientation` (from the screen, defaulting to `landscape`) and `ppi` (from the device row). `width`/`height` still read `dimension_width`/`dimension_height`, columns DROPPED from `earner_screen_devices` by migration `20260527180000`, so they resolve to undefined and are omitted from the JSON entirely — registered pixels now live in `screen_width_px`/`screen_height_px`. Physical inches you send as `width_in`/`height_in` are stored but never echoed here. */
|
|
788
919
|
display?: Record<string, unknown>;
|
|
920
|
+
/** Static, unsigned player URL `https://screen.trillboards.com?fp=<fingerprint>`. It never expires or rotates, and is `null` until a screen is linked. */
|
|
789
921
|
embed_url?: string;
|
|
922
|
+
/** Venue-enrichment state. `status` reads `complete` only when a classification source exists AND `location` actually resolved, and a partner-declared venue category alone sets that source — so `complete` does not mean AI classification ran. `confidence` is dead: `venue_classification_confidence` has no column and no writer in the API, so it is always omitted. */
|
|
790
923
|
enrichment?: Record<string, unknown>;
|
|
924
|
+
/** Verbatim echo of the `device_id` you sent in the registration body (`partner_external_id`). It is the per-partner idempotency key: re-registering the same value updates the existing device rather than creating another. */
|
|
791
925
|
external_device_id?: string;
|
|
926
|
+
/** Deterministic device key: `P_` plus the first 24 hex chars of `md5("<your partner id>_<external_device_id>")`. It never rotates, and it is the token embedded in `embed_url` as well as the heartbeat routing key. */
|
|
792
927
|
fingerprint?: string;
|
|
928
|
+
/** Resolved geo: the enriched screen row first, falling back to the registration-time device row, with placeholder strings coalesced away. `lat`/`lng` coalesce as a pair, `venue_type` is the OpenOOH child slug, and `timezone` alone is read device-first. */
|
|
793
929
|
location?: Record<string, unknown>;
|
|
930
|
+
/** The linked screen's name, falling back to the raw `external_device_id` when the screen has none. A registration that omits `name` seeds the screen as `Partner Device: <device_id>`. */
|
|
794
931
|
name?: string;
|
|
932
|
+
/** The linked screen's Mongo ObjectId hex (`earner_screens.mongo_earnerscreen_id`). This is the id every non-device partner endpoint takes — display settings, screen groups, fleet commands, analytics. Absent when no screen is linked. */
|
|
795
933
|
screen_id?: string;
|
|
934
|
+
/** The linked screen's `earner_screens.status` verbatim. Partner registration auto-approves, so the live value is `approved` (`deactivated` after a DELETE, `pending` only while no screen is linked) — not the `pending`/`active`/`offline`/`suspended` vocabulary the `Device` schema declares, whose mapper has no production call site. */
|
|
796
935
|
status?: string;
|
|
936
|
+
/** `earner_screen_devices.updated_at`, stamped `NOW()` by the device-mirror write buffer on every flush including heartbeats — a liveness signal, not a configuration-change marker. */
|
|
797
937
|
updated_at?: string;
|
|
938
|
+
/** OpenOOH classification rebuilt from the typed `screen_venue_taxonomy` row: `category` is the PARENT slug, `subcategory` the child, `openooh_venue_type_id` the numeric child id. Empty until classification lands, unless registration supplied a resolvable venue category. */
|
|
798
939
|
venue?: Record<string, unknown>;
|
|
799
940
|
};
|
|
800
941
|
type DisplaySettingsInput = {
|
|
@@ -819,30 +960,45 @@ type DisplaySettingsInput = {
|
|
|
819
960
|
volume?: number;
|
|
820
961
|
};
|
|
821
962
|
type DisplaySettingsResponse = {
|
|
963
|
+
/** NOT EMITTED and NOT WRITABLE — the identifier does not occur anywhere in the API source. The nearest real controls are `ad_interval_seconds` on this resource (rotation cadence, 30-300s) and the slot duration the playback policy derives (clamped 5-60s). */
|
|
822
964
|
ad_duration_default?: number;
|
|
965
|
+
/** NOT EMITTED by this endpoint. `buildAdSurfaces` is computed on a display-settings write, but only to populate the `display_settings_updated` socket event pushed to the device — the HTTP body is the formatter's output and contains no `ad_surfaces`. The declared item shape is wrong too: real surfaces carry `id`, `surface_type`, `enabled`, `display_area_ratio`, `dimensions` and `allowed_ad_types`, never `position`/`width_percent`/`height_percent`. The L-bar PUT is the one HTTP response that does return them. */
|
|
823
966
|
ad_surfaces?: Array<{
|
|
824
967
|
height_percent?: number;
|
|
825
968
|
id?: string;
|
|
826
969
|
position?: string;
|
|
827
970
|
width_percent?: number;
|
|
828
971
|
}>;
|
|
972
|
+
/** NOT EMITTED and NOT WRITABLE. The writer's ten-key allow-list has no `brightness`, so a PUT silently drops it, and the formatter never returns one. Brightness exists in this product only as inbound device telemetry on the heartbeat, which this resource cannot set. */
|
|
829
973
|
brightness?: number;
|
|
830
974
|
content_fit?: ContentFitSettings;
|
|
975
|
+
/** Real and emitted: `fullscreen` (default), `l-bar` or `pip`, read from the typed `display_mode` column in preference to the legacy `display_preferences` JSONB. Note the writer validates a THREE-value enum including `pip`, while the L-bar resource's own `display_mode` accepts only `fullscreen`/`l-bar`. */
|
|
831
976
|
display_mode?: string;
|
|
977
|
+
/** NOT EMITTED and NOT WRITABLE — the identifier does not occur anywhere in the API source. What actually plays when no ad is available is the screen's default streams, managed through the default-streams routes. */
|
|
832
978
|
idle_content?: string;
|
|
979
|
+
/** NOT EMITTED and NOT WRITABLE. Use `sound_enabled: false` instead — that key the formatter does return, defaulting to `true`. */
|
|
833
980
|
mute?: boolean;
|
|
981
|
+
/** Always `display_settings`. The response is FLAT — no `success`/`data` envelope — and it comes from exactly one formatter shared by the GET, PUT and PATCH, so all three return an identical shape. */
|
|
834
982
|
object?: string;
|
|
983
|
+
/** NOT EMITTED under this name — the formatter returns `screen_orientation` instead (`landscape`, `portrait`, `mixed` or `unknown`, defaulting to `landscape`). The writer's allow-list also only accepts `screen_orientation`, so an `orientation` key in a PUT body is silently dropped. */
|
|
835
984
|
orientation?: string;
|
|
985
|
+
/** NOT EMITTED by this endpoint, for the same reason as `ad_surfaces` — it is computed on a write only to fill the device socket event. The declared `max_duration_seconds`/`min_break_between_ads`/`allow_skip` keys do not exist in the real policy either; `buildPlaybackPolicy` produces `policy_version`, millisecond intervals, `max_paid_share`, `surface_policy`, `schedule_policy`, `pip_settings` and `lbar_settings`. */
|
|
836
986
|
playback_policy?: {
|
|
837
987
|
allow_skip?: boolean;
|
|
838
988
|
max_duration_seconds?: number;
|
|
839
989
|
min_break_between_ads?: number;
|
|
840
990
|
};
|
|
991
|
+
/** NOT EMITTED and NOT WRITABLE — the identifier does not occur anywhere in the API source. */
|
|
841
992
|
qr_code_position?: string;
|
|
993
|
+
/** Echo of the server-assigned `req.requestId`, attached only when one exists — the key is absent, not null, otherwise. */
|
|
842
994
|
request_id?: string;
|
|
995
|
+
/** Echoed from the `{screenId}` path parameter rather than read from a column. */
|
|
843
996
|
screen_id?: string;
|
|
997
|
+
/** NOT EMITTED and NOT WRITABLE — the identifier `transition_effect` does not occur anywhere in the API source. There is no transition control on this resource. */
|
|
844
998
|
transition_effect?: string;
|
|
999
|
+
/** ISO-8601 of the SCREEN row's `updatedAt`, not a display-settings-specific timestamp; when the screen has none the formatter substitutes the current time, so it is never null and never proves a write landed. */
|
|
845
1000
|
updated_at?: string;
|
|
1001
|
+
/** NOT EMITTED and NOT WRITABLE — the string `volume` does not appear in the display-settings service or the formatter at all. The only audio control on this resource is the boolean `sound_enabled`. */
|
|
846
1002
|
volume?: number;
|
|
847
1003
|
};
|
|
848
1004
|
/**
|
|
@@ -941,6 +1097,7 @@ type EarnerScreens = {
|
|
|
941
1097
|
neighborhood?: string | null;
|
|
942
1098
|
ntp_sync_required: boolean;
|
|
943
1099
|
offline_alert_sent_at?: string | null;
|
|
1100
|
+
offline_push_alert_sent_at?: string | null;
|
|
944
1101
|
operating_hours?: Record<string, unknown> | null;
|
|
945
1102
|
partner_mongo_id?: string | null;
|
|
946
1103
|
peak_hours?: Array<string> | null;
|
|
@@ -1008,6 +1165,7 @@ type EarnerScreens = {
|
|
|
1008
1165
|
width_inches?: number | null;
|
|
1009
1166
|
};
|
|
1010
1167
|
type EarningsBreakdownResponse = {
|
|
1168
|
+
/** Keyed by `date` for `group_by=day|week` and by `screen_id` (plus a `screen_name`) for `group_by=screen`; the unused key is absent rather than null. Week rows are labelled `<year>-W<ISO week>`. `earnings_cents` is allocated cumulatively so the rows sum exactly to `summary.total_cents`. */
|
|
1011
1169
|
breakdown?: Array<{
|
|
1012
1170
|
/** Date or screen_id depending on group_by */
|
|
1013
1171
|
date?: string;
|
|
@@ -1016,15 +1174,18 @@ type EarningsBreakdownResponse = {
|
|
|
1016
1174
|
/** Screen ID (when grouped by screen) */
|
|
1017
1175
|
screen_id?: string;
|
|
1018
1176
|
}>;
|
|
1177
|
+
/** **Both fields are hardcoded** — `estimated_cents` is always `0` and `confirmation_date` always `null`. Read `EarningsResponse.pending_confirmation_cents` for the real Net-60 pending figure. */
|
|
1019
1178
|
pending_programmatic?: {
|
|
1020
1179
|
confirmation_date?: string;
|
|
1021
1180
|
estimated_cents?: number;
|
|
1022
1181
|
};
|
|
1182
|
+
/** `start`/`end` echo the request; with neither supplied the window is the last 30 days ending now. The end bound is EXCLUSIVE in the underlying query (`period_date < end`), so passing today's date omits today. */
|
|
1023
1183
|
period?: {
|
|
1024
1184
|
end?: string;
|
|
1025
1185
|
group_by?: "day" | "week" | "screen";
|
|
1026
1186
|
start?: string;
|
|
1027
1187
|
};
|
|
1188
|
+
/** Integer US cents. **`direct_cents` is a hardcoded `0`** and `programmatic_cents` is simply `total_cents`, so the split conveys nothing — everything this rail reports is booked as programmatic. */
|
|
1028
1189
|
summary?: {
|
|
1029
1190
|
direct_cents?: number;
|
|
1030
1191
|
programmatic_cents?: number;
|
|
@@ -1038,6 +1199,7 @@ type EarningsBreakdownResponse = {
|
|
|
1038
1199
|
*
|
|
1039
1200
|
*/
|
|
1040
1201
|
type EarningsData = {
|
|
1202
|
+
/** **Never emitted.** No partner earnings endpoint computes a CPM; the earnings chart returns impressions and cents only, so a caller must divide for itself. */
|
|
1041
1203
|
average_cpm: number;
|
|
1042
1204
|
/** Optional per-day earnings/impressions series. */
|
|
1043
1205
|
daily?: Array<{
|
|
@@ -1046,22 +1208,29 @@ type EarningsData = {
|
|
|
1046
1208
|
earnings: number;
|
|
1047
1209
|
impressions: number;
|
|
1048
1210
|
}>;
|
|
1211
|
+
/** **Never emitted**, so its unit is undefined. Every money field the partner earnings endpoints actually return is integer US cents and carries a `_cents` suffix. */
|
|
1049
1212
|
last_payout_amount?: number;
|
|
1213
|
+
/** **Never emitted.** No handler populates it — read `GET /v1/partner/earnings/transactions?type=payout`, whose rows carry `created_at` and `amount_cents`. */
|
|
1050
1214
|
last_payout_date?: string;
|
|
1051
1215
|
/** Balance not yet paid out. */
|
|
1052
1216
|
pending_payout: number;
|
|
1217
|
+
/** Declared for the SDK type but **not emitted**: `GET /v1/partner/connect/earnings` returns `{summary, chart, period}` where `period` is an OBJECT (`days`/`start`/`end`), not this string. */
|
|
1053
1218
|
period: string;
|
|
1219
|
+
/** **Not emitted under this name**, so its unit is undefined. The live earnings chart returns `summary.total_earnings_cents` — integer US cents — summed over the requested window from the payout-accounting rollup of `programmatic_earnings_daily`. */
|
|
1054
1220
|
total_earnings: number;
|
|
1221
|
+
/** **Not emitted under this name.** The live earnings chart returns `summary.total_impressions`, the impression count carried on the same daily earner-revenue rows the cents are summed from. */
|
|
1055
1222
|
total_impressions: number;
|
|
1056
1223
|
};
|
|
1057
1224
|
type EarningsResponse = {
|
|
1058
1225
|
/** Amount available for immediate payout */
|
|
1059
1226
|
available_for_payout_cents?: number;
|
|
1227
|
+
/** Integer US cents, and only one component is real: **`direct_campaigns` and `programmatic_floodgates` are hardcoded `0`**, while `programmatic_google` is ALL programmatic earner revenue for the current calendar month regardless of provider — it is not Google-specific, and unlike the lifetime figures above it is month-to-date. */
|
|
1060
1228
|
breakdown?: {
|
|
1061
1229
|
direct_campaigns?: number;
|
|
1062
1230
|
programmatic_floodgates?: number;
|
|
1063
1231
|
programmatic_google?: number;
|
|
1064
1232
|
};
|
|
1233
|
+
/** `amount_cents` is integer US cents and simply repeats `available_for_payout_cents`; `date` is the 1st of next month, computed in the handler with no reference to any scheduled transfer; `stripe_status` is the partner's Connect account status, which must be `active` before a payout request is accepted. */
|
|
1065
1234
|
next_payout_estimate?: {
|
|
1066
1235
|
amount_cents?: number;
|
|
1067
1236
|
date?: string;
|
|
@@ -1120,16 +1289,21 @@ type ErrorCodeResponse = {
|
|
|
1120
1289
|
type ErrorEntry = {
|
|
1121
1290
|
/** Stable machine error code (e.g. not_found). */
|
|
1122
1291
|
code: string;
|
|
1292
|
+
/** Derived mechanically as `https://api.trillboards.com/docs/errors#<code>`, so it is always well-formed whether or not that anchor exists. It appears on live error envelopes and catalog entries; no per-partner error-log endpoint returns it. */
|
|
1123
1293
|
doc_url: string;
|
|
1124
1294
|
/** The API path that produced the error. */
|
|
1125
1295
|
endpoint: string;
|
|
1296
|
+
/** **No producer.** `GET /v1/errors` returns the static error CATALOG (code definitions in the abstract), not per-partner occurrences, and the per-request metadata the API does retain is keyed by `request_id` and carries no `id`. */
|
|
1126
1297
|
id: string;
|
|
1298
|
+
/** **Never emitted as part of a logged occurrence.** Human-readable text appears only on a live error envelope or a catalog entry; the retained per-request record stores `error_code`/`error_type` and no message. */
|
|
1127
1299
|
message: string;
|
|
1128
1300
|
/** The HTTP method of the failing request. */
|
|
1129
1301
|
method: string;
|
|
1130
1302
|
/** The request parameter the error concerns, if any. */
|
|
1131
1303
|
param?: string;
|
|
1304
|
+
/** The `req_`-prefixed identifier of the failing request, minted server-side and returned both on the `X-Request-Id` header and inside every error envelope. It is the only handle that links a caller-side failure to server-side request metadata. */
|
|
1132
1305
|
request_id: string;
|
|
1306
|
+
/** **Not emitted by any error endpoint.** The per-request metadata the API retains does stamp an ISO timestamp at response time, but it lives in a bounded in-memory LRU cache, not a queryable error log. */
|
|
1133
1307
|
timestamp: string;
|
|
1134
1308
|
/** Stripe-style error taxonomy (matches the SDK's ApiErrorType). */
|
|
1135
1309
|
type: "invalid_request_error" | "authentication_error" | "permission_error" | "rate_limit_error" | "idempotency_error" | "api_error";
|
|
@@ -1169,13 +1343,19 @@ type ErrorType = "api_error" | "authentication_error" | "idempotency_error" | "i
|
|
|
1169
1343
|
type ExtendedIdentity = {
|
|
1170
1344
|
/** OpenRTB person-based identifier type. */
|
|
1171
1345
|
atype: 3;
|
|
1346
|
+
/** When the token stops being usable — for a UID2 token this is the refresh deadline, and it is stored verbatim regardless of `status` (unlike the advertising-identity envelope, which discards `expires_at` on an `available` state). `null` means no declared expiry, not "already expired". */
|
|
1172
1347
|
expires_at?: string | null;
|
|
1348
|
+
/** The identity value itself, up to 4096 characters and containing no whitespace. Omit it (or send `null`) with `unavailable` to CLEAR a previously stored identity — that is the only way to retract one; sending the envelope with `available` and an empty id is rejected instead. */
|
|
1173
1349
|
id?: string | null;
|
|
1350
|
+
/** `hem_sha256` must be exactly 64 hexadecimal characters and is lowercased ONCE server-side and never re-hashed — send the digest, not the email. `uid2_token` is an opaque case-sensitive token forwarded byte-for-byte with only a whitespace check, so do not trim, lowercase or re-encode it. Together with `source` this pair is the row key: one identity per `(id_type, source)` per device. */
|
|
1174
1351
|
id_type: "hem_sha256" | "uid2_token";
|
|
1352
|
+
/** When the device observed this identity — and it is the CONFLICT-RESOLUTION CLOCK, not just provenance: the upsert applies only when this timestamp is greater than or equal to the one already stored, so a replayed or out-of-order envelope is silently discarded with no error. An unparseable value rejects the whole array. */
|
|
1175
1353
|
observed_at: string;
|
|
1176
1354
|
/** Canonical top-level domain for this identity system. */
|
|
1177
1355
|
source: string;
|
|
1356
|
+
/** Version of the SDK or integration that produced this identity, up to 128 characters, kept for provenance only — no server logic branches on it. It is trimmed, and a whitespace-only string is stored as `null` rather than as an empty string. */
|
|
1178
1357
|
source_version?: string | null;
|
|
1358
|
+
/** `available` requires `id`; `unavailable` requires `id` to be ABSENT — sending both is a validation failure, and because normalisation is all-or-nothing across the array that single bad element discards every other identity in the same request while the heartbeat still returns 200. */
|
|
1179
1359
|
status: "available" | "unavailable";
|
|
1180
1360
|
};
|
|
1181
1361
|
/**
|
|
@@ -1184,27 +1364,36 @@ type ExtendedIdentity = {
|
|
|
1184
1364
|
*
|
|
1185
1365
|
*/
|
|
1186
1366
|
type FleetCommand = {
|
|
1367
|
+
/** The device command that was fanned out — one of `reload`, `update_settings`, `restart`, `screenshot`, `clear_cache`, `update_content`, `refresh_ads`, `update_config`, `get_state`, `show_ad`, `play_ad`. Validated in the application against `ALLOWED_COMMANDS`, with no database constraint, so the allow-list can grow without a schema migration. */
|
|
1187
1368
|
command_type?: string;
|
|
1369
|
+
/** Set on BOTH terminal states — `completed` and `failed` — so it is not a success marker. `null` only while the job has not reached a terminal state. */
|
|
1188
1370
|
completed_at?: string | null;
|
|
1371
|
+
/** When the job row was inserted, i.e. when the batch was accepted. Because execution is synchronous, `created_at` and `completed_at` bracket the entire fan-out. */
|
|
1189
1372
|
created_at?: string;
|
|
1373
|
+
/** The raw thrown message, set only when the JOB itself failed; `null` otherwise. Per-screen dispatch failures do NOT populate it — those live in `results[].error`, and a job whose targets all failed still reports `status: "completed"` with `failed` > 0. */
|
|
1190
1374
|
error?: string | null;
|
|
1191
1375
|
/** Screens that could not be enqueued */
|
|
1192
1376
|
failed?: number;
|
|
1377
|
+
/** The `sg_<uuid>` screen group this batch fanned out to. It is a plain TEXT reference, not a foreign key: groups are only ever soft-deleted, so the job outlives its group and keeps the id as an audit record. */
|
|
1193
1378
|
group_id?: string | null;
|
|
1194
|
-
/** Job id (fc_
|
|
1379
|
+
/** Job id: the literal prefix `fc_` followed by a v4 UUID (`pgRepo/fleetCommands.js` mints `fc_${crypto.randomUUID()}`). Opaque — do not parse the UUID half. */
|
|
1195
1380
|
id?: string;
|
|
1381
|
+
/** Constant string `fleet_command` — the Stripe-style type discriminator, present both here and inside the `data` of a `fleet_command.*` webhook. */
|
|
1196
1382
|
object?: string;
|
|
1197
1383
|
/** Screens the command was enqueued to */
|
|
1198
1384
|
queued?: number;
|
|
1385
|
+
/** One entry per screen the dispatch touched, persisted only when the fan-out actually produced rows. `results[].status` is `queued` or `failed` — the per-screen dispatch outcome, a different vocabulary from the job's own `status` — and `device_command_id` is the queued device-command's Mongo ObjectId, or `null` when nothing was queued (e.g. `error: "No device linked"`). On a `fleet_command.failed` webhook this is always `[]`, because the dispatch never completed. */
|
|
1199
1386
|
results?: Array<{
|
|
1200
1387
|
device_command_id?: string | null;
|
|
1201
1388
|
error?: string | null;
|
|
1202
1389
|
screen_id?: string;
|
|
1203
1390
|
status?: "queued" | "failed";
|
|
1204
1391
|
}>;
|
|
1392
|
+
/** Execution is synchronous inside the create request — the job walks `queued` to `running` to `completed` (or `failed`) before the POST returns — so a poll will in practice only ever observe a terminal value. `queued` and `running` exist for a future async worker that claims `queued` rows. */
|
|
1205
1393
|
status?: "queued" | "running" | "completed" | "failed";
|
|
1206
1394
|
/** Member screens resolved for this command */
|
|
1207
1395
|
targeted?: number;
|
|
1396
|
+
/** Bumped on each job state transition (`queued` to `running` to terminal), so on a completed job it is effectively the moment the fan-out finished. */
|
|
1208
1397
|
updated_at?: string;
|
|
1209
1398
|
};
|
|
1210
1399
|
/**
|
|
@@ -1218,8 +1407,11 @@ type FleetCommand = {
|
|
|
1218
1407
|
type FleetCommandEvent = {
|
|
1219
1408
|
/** The fleet_command job object. */
|
|
1220
1409
|
data?: Record<string, unknown>;
|
|
1410
|
+
/** The terminal state that fired, also sent in the `X-Trillboards-Event` header and mirrored to the SSE stream. Note `fleet_command.completed` fires even when individual screens failed — check `data.failed` and `data.results[]`. Delivery is gated by the `SCREEN_GROUP_EVENTS_ENABLED` runtime flag, off by default. */
|
|
1221
1411
|
event?: "fleet_command.completed" | "fleet_command.failed";
|
|
1412
|
+
/** `evt_<base36 epoch ms>_<12 hex>`, minted once per occurrence and reused across retries of that delivery — so it is the idempotency key. The same id also goes to every endpoint subscribed to the event, so dedupe per (endpoint, id). It is not the delivery audit id and is not queryable. */
|
|
1222
1413
|
id?: string;
|
|
1414
|
+
/** Stamped when the server enqueued the delivery, not when it was sent; retries keep the original value. It is the emit time of the terminal-state event, which is not the same as the job's `completed_at`. */
|
|
1223
1415
|
timestamp?: string;
|
|
1224
1416
|
};
|
|
1225
1417
|
/**
|
|
@@ -1227,6 +1419,7 @@ type FleetCommandEvent = {
|
|
|
1227
1419
|
*
|
|
1228
1420
|
*/
|
|
1229
1421
|
type HeaderBiddingSettings = {
|
|
1422
|
+
/** `null` unless the screen falls in the ad-buffering rollout bucket. `min_buffer`, `max_buffer` and `warmup_count` are COUNTS OF PREFETCHED ADS (defaults 3 / 5 / 5), not seconds or bytes; `max_cache_age_seconds` (default 300) is how long a prefetched VAST may be held before it must be discarded. */
|
|
1230
1423
|
ad_buffering?: {
|
|
1231
1424
|
enabled?: boolean;
|
|
1232
1425
|
max_buffer?: number;
|
|
@@ -1234,18 +1427,31 @@ type HeaderBiddingSettings = {
|
|
|
1234
1427
|
min_buffer?: number;
|
|
1235
1428
|
warmup_count?: number;
|
|
1236
1429
|
} | null;
|
|
1430
|
+
/** `null` unless the screen falls in the ad-pod experiment bucket. When present it carries `enabled`, `pod_size` (how many ads to request in a single pod) and `variant_name` (the pod-size arm this screen hashed into) — the arm is assigned server-side, not chosen by the client. */
|
|
1237
1431
|
ad_pod?: Record<string, unknown> | null;
|
|
1432
|
+
/** Share of ad slots the client should give to programmatic, as a decimal FRACTION 0-1 (`0.20` = 20%), defaulting to `1.0`. Same underlying value the CMS exposes as `programmatic_allocation_percent` (0-100, i.e. this times 100). Only honoured when `request_mode` is `allocation`; under `always` it is ignored. */
|
|
1238
1433
|
allocation?: number;
|
|
1434
|
+
/** One id per waterfall BUILD (one `/ads` response), shared by every entry in `vast_waterfall.sources[]`; the narrower per-source key is `request_id`. This is the value that lands in `vast_requests.auction_request_id`, so it is what groups all the source attempts belonging to a single ad opportunity. Echo it, never mint your own. */
|
|
1239
1435
|
auction_request_id?: string;
|
|
1436
|
+
/** Constant `true` whenever this object is present — the whole `header_bidding_settings` object is `null` for a screen that is not programmatically eligible. This is header-bidding *configuration*, not the serving master switch: that is `programmatic_enabled` (surfaced as `ProgrammaticSettingsResponse.enabled`) plus any active ads-disabled reason, and the legacy `enabled` shadow was deliberately removed from the serving decision. */
|
|
1240
1437
|
enabled?: boolean;
|
|
1438
|
+
/** `+`-joined tags for the yield experiments this screen is bucketed into (e.g. `yield_buffer_treatment+ad_pod_pod_3`), covering ad buffering, fast loop and ad pods; `null` when none is active. Assignment is a consistent hash of the screen id, so it is stable for a screen across requests. Report it back for analytics — on its own it changes no client behaviour. */
|
|
1241
1439
|
experiment_variant?: string | null;
|
|
1440
|
+
/** `null` unless the screen falls in the fast-loop experiment bucket. When present it carries `enabled` and `ad_to_ad_gap_ms` — the gap to leave between consecutive ads, in MILLISECONDS (default 5000), which is the experiment's shortened alternative to the normal ~30s cadence. */
|
|
1242
1441
|
fast_loop?: Record<string, unknown> | null;
|
|
1442
|
+
/** No live producer — `null` on every response. The only assignment is guarded on a partner-level `fetchMode` setting, and no demand partner in the registry defines one, so this key is never populated at either the top level or per source. Do not branch on it. */
|
|
1243
1443
|
fetch_mode?: string | null;
|
|
1444
|
+
/** Legacy predecessor of `request_mode`, retained only as the fallback that selects `always` (true) versus `allocation` (false) when no explicit `request_mode` is stored. An explicit `request_mode` always wins — do not read the two independently or you will disagree with the server on screens that set both. */
|
|
1244
1445
|
force_request?: boolean;
|
|
1446
|
+
/** `null` except on screens in a revenue-recovery experiment arm, where it carries `name` and `reason` plus the IMA overrides to apply for that arm — some subset of `num_redirects`, `vast_load_timeout_ms`, `load_video_timeout_ms`, `source_timeout_ms`, `mime_types`, `enable_preloading`, `sdk_version_override` and `client_hints_override`. The key set is deliberately open: apply the keys you recognise and ignore the rest. */
|
|
1245
1447
|
ima_profile?: Record<string, unknown> | null;
|
|
1448
|
+
/** Minimum gap the client should leave between VAST requests, in SECONDS (not milliseconds), default `30`, accepted range 0-300. Advisory: no server-side check enforces it — the `/ads` rate limiter is a separate mechanism that does not read this value. */
|
|
1246
1449
|
min_interval_seconds?: number;
|
|
1450
|
+
/** Always `null`. Every stored value for this key is JSON `null`, it has no typed home on the screen record (dropped from the typed read path), and the builder emits `hbs.prebid || null` — there is no live producer. Prebid Server demand, when it participates, is spliced into `vast_waterfall.sources[]`, not surfaced here. */
|
|
1247
1451
|
prebid?: Record<string, unknown> | null;
|
|
1452
|
+
/** Opaque public alias of the first waterfall source — the same string as `variant_name` and `source_name`, not the `floodgates`/`prebid`/`other` bidding-provider value the CMS accepts on `ProgrammaticSettingsInput.provider`. The key is absent entirely when the waterfall resolved no sources. */
|
|
1248
1453
|
provider?: string;
|
|
1454
|
+
/** Player tuning selected purely from the screen's `country`; today only `IN` differs from the default — `timeout_ms` 25000 vs 12000, `fallback_timeout_ms` 35000 vs 18000, `vquality` 1 vs 2, `enable_network_adaptation` true vs false, and `ad_buffer_percentage` as a 0-1 fraction of network capacity (0.25 vs 1.0). Note the wire shapes: `preferred_resolution` is an object `{width, height}` and `vquality` an integer, not the strings declared here. */
|
|
1249
1455
|
regional_config?: {
|
|
1250
1456
|
ad_buffer_percentage?: number;
|
|
1251
1457
|
enable_network_adaptation?: boolean;
|
|
@@ -1256,21 +1462,30 @@ type HeaderBiddingSettings = {
|
|
|
1256
1462
|
} | null;
|
|
1257
1463
|
/** Per-request waterfall identifier; echo it back on programmatic-event reports. */
|
|
1258
1464
|
request_id?: string;
|
|
1465
|
+
/** `allocation` = request only on the `allocation` share of slots; `always` = request on every slot and ignore `allocation`. These are the only two accepted values. Defaults to `always` on the static-tag path; on the waterfall path it defaults from the legacy `force_request` boolean (`always` when true, `allocation` when false). */
|
|
1259
1466
|
request_mode?: string;
|
|
1467
|
+
/** Opaque alias of the source these top-level fields describe — the first waterfall source, identical to `provider`/`variant_name`. Absent when the waterfall resolved no sources. */
|
|
1260
1468
|
source_name?: string | null;
|
|
1469
|
+
/** The first source's `priority` sort key, copied up to the top level. Same semantics as `VastWaterfallSource.priority`: an opaque ascending key that may start at 0 and may be non-contiguous — it is not an index, a count, or a position you can do arithmetic on. */
|
|
1261
1470
|
source_priority?: number | null;
|
|
1471
|
+
/** Opaque public alias of the first waterfall source, byte-identical to `provider` and `source_name` on the same response. Format is `src_<6 hex>` (SHA-256 of the partner key) or `var_<6 hex>` (of the variant name), plus the fixed `src_test` for the sandbox test source and `inbound_dsp` for a collapsed DSP-seat source. Never a real partner name, and it may be rotated per tenant — echo it back on `programmatic-event` reports rather than persisting it as an identity. */
|
|
1262
1472
|
variant_name?: string;
|
|
1473
|
+
/** Client-side VAST/creative caching directive, `null` unless the fleet-level VAST-cache switch is on (shipped default: off). `ttl_seconds` defaults to 21600 (6 hours). `priority` is a fixed rank of `1.5` — between paid ads (1) and self-promo (2) — and is emitted as a NUMBER despite the string type declared here. */
|
|
1263
1474
|
vast_cache?: {
|
|
1264
1475
|
enabled?: boolean;
|
|
1265
1476
|
priority?: string;
|
|
1266
1477
|
ttl_seconds?: number;
|
|
1267
1478
|
} | null;
|
|
1479
|
+
/** Convenience copy of `vast_waterfall.sources[0].vast_url` — the server overwrites it with the first source on every build and drops the key entirely when the waterfall resolved no sources. It is not an extra tag, so fetching only this URL is exactly the `sources[0]`-and-stop defect the `VastWaterfall` contract warns about. */
|
|
1268
1480
|
vast_tag_url?: string;
|
|
1269
1481
|
vast_waterfall?: VastWaterfall;
|
|
1270
1482
|
} | null;
|
|
1271
1483
|
type HttpProbeResult = {
|
|
1484
|
+
/** IP address that was probed. The target list is PARASITIC on mDNS — the hosts discovered in the same cycle plus the Wi-Fi gateway, de-duplicated and capped at 10. There is no subnet scan, so a device that advertises nothing on mDNS is never probed. */
|
|
1272
1485
|
host: string;
|
|
1486
|
+
/** Always `80` — the probe only ever tries plain HTTP on port 80, and the value is a literal at the call site rather than something the scanner chose. Printer ports 515 and 9100-9102 are refused before any socket opens, because an HTTP request to a JetDirect/LPD port makes the printer physically spool the bytes. */
|
|
1273
1487
|
port?: number | null;
|
|
1488
|
+
/** The HTTP `Server:` response header from a `HEAD /` request (HTTP/1.0, 1.5 s timeout). Never null in an emitted row — a probe that gets no usable header produces no row, and the value is rejected outright if it is over 256 characters or contains an `@`, angle brackets, control characters, or the words `password`/`passwd`/`secret`/`phpinfo`. Failures are counted separately in `skipReasonCounts`, so absence from this array is not evidence the host is down. */
|
|
1274
1489
|
server?: string | null;
|
|
1275
1490
|
[key: string]: unknown;
|
|
1276
1491
|
};
|
|
@@ -1298,13 +1513,17 @@ type Impression = {
|
|
|
1298
1513
|
advertiser_name?: string;
|
|
1299
1514
|
/** Effective CPM for the impression, in the partner's currency. */
|
|
1300
1515
|
cpm?: number;
|
|
1516
|
+
/** Sent on the wire as `did`. The server accepts the device's Mongo id, its fingerprint, or the `partner_external_id` you registered for it. An unrecognised value is logged and the call still returns 200 — only a SIGNED impression 404s on an unknown device. */
|
|
1301
1517
|
device_id: string;
|
|
1302
1518
|
/** Play duration in seconds. */
|
|
1303
1519
|
duration_seconds?: number;
|
|
1304
1520
|
/** Earner revenue accrued for the impression. */
|
|
1305
1521
|
earnings?: number;
|
|
1522
|
+
/** The per-serve id minted by the server on `data.ads[].impression_id` (`imp_<epoch-ms>_<8 hex>`) and echoed back as the `impid` parameter on the impression call — it is the dedup key, so never generate your own. Note that no endpoint currently returns this object: it is the `@trillboards/api-client` type describing what you SEND. */
|
|
1306
1523
|
impression_id: string;
|
|
1524
|
+
/** The batch endpoint reads `timestamp`, NOT `played_at` — a `played_at` you send is ignored and the impression is stamped with server receive time instead. That matters for offline sync: send `timestamp` if the real play time has to survive the upload delay. */
|
|
1307
1525
|
played_at: string;
|
|
1526
|
+
/** Your earner-screen id (24-character hex Mongo ObjectId), sent on the wire as `sid`. It is only a FALLBACK for resolving the screen — the server resolves from the device (`did`) first and reads `sid` solely when no device matched. */
|
|
1308
1527
|
screen_id: string;
|
|
1309
1528
|
};
|
|
1310
1529
|
/**
|
|
@@ -1325,12 +1544,19 @@ type LegacyError = {
|
|
|
1325
1544
|
success?: boolean;
|
|
1326
1545
|
};
|
|
1327
1546
|
type MdnsNetworkDevice = {
|
|
1547
|
+
/** The responder's IP address as a string — the first IPv4 address if it has one, otherwise any address. A record that resolves to no address is dropped rather than emitted with a null host, so in practice this is always populated. It is also what seeds the HTTP-probe target list for the same discovery cycle. */
|
|
1328
1548
|
host?: string | null;
|
|
1549
|
+
/** The service instance name verbatim from the resolve event. A PII filter runs first and DROPS THE WHOLE ROW when the name looks like a personal name (an apostrophe-s possessive, an email, a MAC), so devices named after people disappear from the list entirely rather than appearing anonymised. */
|
|
1329
1550
|
instanceName: string;
|
|
1551
|
+
/** Model string lifted from the TXT record using a per-service-type key table — `md` for Chromecast and HAP, `model`/`am` for AirPlay, `ty`/`product` for IPP, `dt` for Matter (a device-TYPE code used as a model surrogate, not a model name), with a broader default list for other types. `null` when no key in that list is present or all their values are blank. */
|
|
1330
1552
|
mdnsModel?: string | null;
|
|
1553
|
+
/** Version string from the TXT record. Frequently this is `txtvers` — the TXT RECORD FORMAT version, not the device firmware version — because that key is in the fallback list for most service types. `null` when no candidate key is present. */
|
|
1331
1554
|
mdnsSoftwareVersion?: string | null;
|
|
1555
|
+
/** Vendor string from the TXT record, by the same per-type key table. Two traps: Chromecast has an EMPTY vendor key list, so it is always `null` there; and for HAP this carries the HomeKit `ci` CATEGORY id (a numeric device class) while for Matter it carries the raw `VP` `vendorid+productid` pair — neither is a vendor name. */
|
|
1332
1556
|
mdnsVendor?: string | null;
|
|
1557
|
+
/** TCP/UDP port advertised in the SRV record. A port of 0 is coerced to `null` rather than forwarded, so `null` means "not advertised", never "port zero". */
|
|
1333
1558
|
port?: number | null;
|
|
1559
|
+
/** The mDNS service type the record resolved under, e.g. `_googlecast._tcp.`. Discovery is not open-ended — the scanner browses a FIXED list of 19 types (Roku, AirPlay, RAOP, Chromecast, Amazon WPlay, Android TV Remote, Spotify Connect, device-info, companion-link, IPP/IPPS/printer/PDL, HAP, Matter, Alexa, HTTP and the Trillboards CSI type), so a device advertising anything else is invisible here. */
|
|
1334
1560
|
serviceType: string;
|
|
1335
1561
|
[key: string]: unknown;
|
|
1336
1562
|
};
|
|
@@ -1341,22 +1567,29 @@ type MdnsNetworkDevice = {
|
|
|
1341
1567
|
*
|
|
1342
1568
|
*/
|
|
1343
1569
|
type MeasurementMethodology = {
|
|
1570
|
+
/** Prose description of the happy-path pipeline: edge capture, buffered ingest, `signal_observations`, a 30-minute device-resolution pass into `resolved_device_observations`, then a 30-minute transition pass. Names the stores only — never infrastructure or source locations. */
|
|
1344
1571
|
canonical_path?: string;
|
|
1345
1572
|
/** What each screen observes and what is dropped at the edge. */
|
|
1346
1573
|
capture_stack?: Record<string, unknown>;
|
|
1347
1574
|
/** Cross-day identity, including measured coverage (1.6% of identifiable BLE rows). An earlier revision said ~31%; that graded the presence of stable_payload_hash and ignored the sanitizer the identity expression applies, and it is withdrawn. */
|
|
1348
1575
|
cross_day_matching?: Record<string, unknown>;
|
|
1576
|
+
/** The in-range contour: BLE RSSI at or above -75 dBm, empirically about 5 m line-of-sight for Class-2 radios at typical mounting. Calibration is static per screen-class — there is no per-screen ranging today — and devices inside the contour classified as stationary are excluded from the audience funnel by default. */
|
|
1349
1577
|
display_exposure_zone?: Record<string, unknown>;
|
|
1578
|
+
/** What promotes a raw in-range observation to a qualifying event, one entry per qualification (`ble_engagement`, `face_attention`, `returning_visitor`), each carrying its definition, its rationale, and the ClickHouse field a reviewer re-derives it from. */
|
|
1350
1579
|
dwell_qualification_thresholds?: Record<string, unknown>;
|
|
1580
|
+
/** Static disclosure of how observation confidence decays: `algorithm` (Bayesian posterior with temporal decay), `decay_window_days` (90) and `max_confidence` (0.95). These are published constants, not per-request measurements. */
|
|
1351
1581
|
edge_confidence_model?: Record<string, unknown>;
|
|
1352
1582
|
/** Evidence grades, which are decisionable, and person-grain resolution. */
|
|
1353
1583
|
evidence_grade_ladder: Record<string, unknown>;
|
|
1584
|
+
/** Prose description of degraded ingest: observations buffer on a durable queue and replay on recovery, and after repeated failures land in a dead-letter store rather than being dropped. */
|
|
1354
1585
|
fallback_path?: string;
|
|
1355
1586
|
/** How identifiers are keyed, and what erasure means. */
|
|
1356
1587
|
hashing?: Record<string, unknown>;
|
|
1357
1588
|
/** The counting UNIT. Reach and unique-device counts are counts of BLE identifiers, not devices and not people; one device contributes several identifiers per window and the number grows with the window. Carries the measured identifiers-per-device per fleet type and the controlled comparison it was derived from. */
|
|
1358
1589
|
identifier_inflation?: Record<string, unknown>;
|
|
1590
|
+
/** Alignment against MRC OOH Phase 2. `mrc_status_accredited` is the machine-readable answer and is **false until an MRC grant reference exists** — read that boolean rather than parsing the English `status` line. Also carries the monotonic qualification chain (`in_range` through `cross_screen`) and the interval method (Wilson score for proportions, 200-sample bootstrap for medians). */
|
|
1359
1591
|
mrc_ooh_phase2_alignment: Record<string, unknown>;
|
|
1592
|
+
/** Version markers for the edge parsers that produced the disclosed fields — `ble_advertisement_parser`, `mdns_txt_extractor`, and the integer `heartbeat_payload_schema`. Hand-maintained constants, bumped when a parser changes. */
|
|
1360
1593
|
parser_versions?: Record<string, unknown>;
|
|
1361
1594
|
/** k-anonymity floor and differential-privacy budget. */
|
|
1362
1595
|
privacy_gates: Record<string, unknown>;
|
|
@@ -1392,25 +1625,39 @@ type Pagination = {
|
|
|
1392
1625
|
total?: number;
|
|
1393
1626
|
};
|
|
1394
1627
|
type Partner = {
|
|
1628
|
+
/** Defaults to the literal `lite_sdk` when unset, so it is never null, and is `lite_sdk` for effectively the whole fleet. It is load-bearing at serve time: `lite_sdk`/`full_sdk` mark the partner a video-only SDK consumer, so an image creative is swapped for the ad's partner video URL unless `sdk_config.allowed_ad_types` includes `image`. */
|
|
1395
1629
|
integration_tier?: string;
|
|
1630
|
+
/** Registered company name. The same string is the buyer-facing media-owner label for this partner's inventory, so it is what advertisers see in the media-owner picker. */
|
|
1396
1631
|
name?: string;
|
|
1632
|
+
/** The 24-character hex Mongo-era identifier (`partners.mongo_partner_id`), not the table's integer `partner_id`. This is the value the API key's owner id and every `partner_mongo_id` foreign key carry. */
|
|
1397
1633
|
partner_id?: string;
|
|
1398
1634
|
/** Legacy classification retained for existing integrations. New integrations should not branch on this field. */
|
|
1399
1635
|
partner_type?: string;
|
|
1636
|
+
/** The partner's share as a percentage 0-100 (the stored rate is 0-1 and is multiplied by 100 here). Resolved from the active, currently-effective `publisher_payout_rates` rule; with no such rule it falls through to the system default and reports `60` — which is also why the `POST /register` response echoes `60` even when the caller asked for 70, since the partner row is read back before the rate row is written. */
|
|
1400
1637
|
revenue_share_percent?: number;
|
|
1401
1638
|
sdk_config?: SDKConfig;
|
|
1639
|
+
/** Lower-cased at registration and unique across all partners (a collision is rejected `409`). It appears in device embed URLs and portal links, so it is effectively permanent once devices are provisioned. */
|
|
1402
1640
|
slug?: string;
|
|
1403
1641
|
stats?: PartnerStats;
|
|
1642
|
+
/** In practice **always `active` here**: the API-key resolver refuses any partner whose status is not `active`, so a `pending`/`suspended`/`terminated` partner gets `401 invalid_api_key` and never reaches this response. Registration also always creates `active`. */
|
|
1404
1643
|
status?: "pending" | "active" | "suspended" | "terminated";
|
|
1644
|
+
/** Defaults to the literal `pending` when unset. `POST /earnings/payout` refuses any value other than `active`. */
|
|
1405
1645
|
stripe_account_status?: string;
|
|
1406
1646
|
};
|
|
1407
1647
|
type PartnerAdDeliveryProfile = {
|
|
1648
|
+
/** Android SDK API level as reported by the device; `null` when not reported, and then no Android rule fires at all. Below 23 forces `mode: vast_fallback` and turns `auto_recovery_enabled` off. 23-25 with desugaring reported off only appends a warning to `notes` and leaves the mode alone. */
|
|
1408
1649
|
android_api_level?: number | null;
|
|
1650
|
+
/** Whether the SERVER may auto-enqueue recovery device commands for this device — the `refresh_ads` and `restart` it issues when a screen heartbeats online without recent VAST requests, or with stalled playback. It is not a client-side setting, and it goes false alongside every `vast_fallback` verdict so we do not restart a device whose player we already know is the wrong one. */
|
|
1409
1651
|
auto_recovery_enabled?: boolean;
|
|
1652
|
+
/** Echoed back from the device's own heartbeat telemetry, or `unknown` when the device never reported one. It selects which compatibility rules run: `android_native` enables the Android API-level gate, `html5_webview` the Chromium-version checks; `disabled` and `unknown` skip both, so no mode change can result from those. */
|
|
1410
1653
|
ima_integration?: "android_native" | "html5_webview" | "disabled" | "unknown";
|
|
1654
|
+
/** The server's per-heartbeat verdict on how this device should play ads: `ima_sdk` is the normal IMA path, `vast_fallback` means bypass IMA entirely and play the VAST in your own fallback player. It is recomputed from the telemetry on every heartbeat, so treat it as a live instruction rather than one-time provisioning. */
|
|
1411
1655
|
mode?: "ima_sdk" | "vast_fallback";
|
|
1656
|
+
/** Human-readable diagnostics accompanying the decision above; an empty array on a healthy device. They are free prose for logs and support tickets, not a stable enum — nothing in the contract depends on matching their text, and their presence alone does not imply degraded delivery. */
|
|
1412
1657
|
notes?: Array<string>;
|
|
1658
|
+
/** Machine token for why `mode` was chosen: `default` (nothing disqualified the device), `sdk_reported_ima_unsupported` (your own telemetry reported `imaSupported: false`), or `android_api_below_23_for_current_ima_android_sdk`. `default` is always what accompanies `ima_sdk`. */
|
|
1413
1659
|
reason?: string;
|
|
1660
|
+
/** Major Chromium version of the device's WebView, `null` when not reported. Advisory only — it never changes `mode`: below 87 appends a content-restoration note and below 80 an additional autoplay/media note. */
|
|
1414
1661
|
webview_chromium_major?: number | null;
|
|
1415
1662
|
};
|
|
1416
1663
|
/**
|
|
@@ -1418,38 +1665,67 @@ type PartnerAdDeliveryProfile = {
|
|
|
1418
1665
|
*/
|
|
1419
1666
|
type PartnerCreativeWarnings = Array<string>;
|
|
1420
1667
|
type PartnerDefaultStream = {
|
|
1668
|
+
/** The stream's 24-hex id, minted server-side with `newObjectId()` at create and stored as `default_streams.mongo_default_stream_id`. It is the value every other default-stream route takes as `{streamId}`; the shape is Mongo's but PostgreSQL is the store of record. */
|
|
1421
1669
|
_id?: string;
|
|
1670
|
+
/** Accepted with no required-field check, same as `google_slides_url`. The derived `canva_design_id` is returned alongside it although this schema does not declare it. */
|
|
1422
1671
|
canva_url?: string;
|
|
1672
|
+
/** Mapped from `default_streams.created_at`. It is the list endpoint's secondary sort key, so two streams sharing a `playback_order` play oldest-first. */
|
|
1423
1673
|
createdAt?: string;
|
|
1674
|
+
/** Falls back to `name`, then to `Background content`; the upload and image routes use `Uploaded video content` / `Image carousel content` instead. */
|
|
1424
1675
|
description?: string;
|
|
1676
|
+
/** Seconds one pass of this stream occupies. It is computed as `images.length x image_duration` when you send images without it; for every other type it is only ever what you sent — nothing probes a YouTube or uploaded video to fill it in on this path. */
|
|
1425
1677
|
duration_seconds?: number;
|
|
1678
|
+
/** The URL the player actually loads into the iframe. It defaults to `generic_iframe_url` when you do not send it, so on an iframe stream the two are usually equal. */
|
|
1426
1679
|
embed_url?: string;
|
|
1680
|
+
/** The iframe source for the `generic_iframe` type. It also seeds `embed_url` when that is omitted, so sending only this one populates both fields. */
|
|
1427
1681
|
generic_iframe_url?: string;
|
|
1682
|
+
/** Unlike `youtube`/`twitch`/`uploaded`/`images`, the `google_slides` type has NO required-field check — a create naming that type with no URL is accepted, stores null, and the screen then renders an empty frame. */
|
|
1428
1683
|
google_slides_url?: string;
|
|
1684
|
+
/** Only five keys survive: `auto_advance`, `slide_delay_ms`, `refresh_interval_seconds`, `sandbox_flags`, `allow_flags` — anything else is silently discarded and a non-object is discarded whole. On update it is MERGED over the stored config rather than replacing it, so a key cannot be cleared by omitting it. */
|
|
1429
1685
|
iframe_config?: Record<string, unknown>;
|
|
1686
|
+
/** Seconds each image is held before advancing. The multipart image route defaults it to 10 when omitted and uses it to compute `duration_seconds`; the JSON create leaves it unset unless you send it. */
|
|
1430
1687
|
image_duration?: number;
|
|
1688
|
+
/** Ordered image URLs, each held for `image_duration` seconds. Required and non-empty when `stream_type` is `images` (400 otherwise), null for every other type; the create accepts a real array, a JSON-encoded array string, or a comma-separated string. */
|
|
1431
1689
|
images?: Array<string>;
|
|
1690
|
+
/** Soft-deactivation flag. The list filters `is_active = true`, so it is always `true` on anything this API returns; the partner DELETE removes the row outright rather than clearing this flag, which is instead used by the screen-wide deactivate paths. */
|
|
1432
1691
|
is_active?: boolean;
|
|
1692
|
+
/** At most one stream per screen carries this — promoting or reordering clears it on every sibling first. A create sets it on the new stream, `reorder` gives it to `order[0]`, and deleting the current stream hands it to whatever the ordering resolves next. */
|
|
1433
1693
|
is_currently_playing?: boolean;
|
|
1694
|
+
/** Falls back to `description`, then to the literal `Default Stream`, so it is never empty. The multipart upload route instead falls back to the uploaded file's original name, and the multi-image route to `Image Carousel (N images)`. */
|
|
1434
1695
|
name?: string;
|
|
1696
|
+
/** Present ONLY on the 201 from the three create routes, which echo the in-memory document back. `default_streams` has no partner column, so list, update and read responses omit this key entirely — tenancy is enforced at the screen, not on the stream row. */
|
|
1435
1697
|
partner_id?: string;
|
|
1698
|
+
/** 1-based position in the screen's rotation; the list sorts on it and breaks ties with `createdAt`. A newly created stream is promoted to 1 and every sibling shifts down, and `POST .../reorder` rewrites it to each id's index in the `order` array you send. */
|
|
1436
1699
|
playback_order?: number;
|
|
1700
|
+
/** The owning screen, mapped from `default_streams.screen_mongo_id` — the same 24-hex id you passed as `{screenId}` in the path. Note the camelCase spelling inside an otherwise snake_case object. */
|
|
1437
1701
|
screenId?: string;
|
|
1702
|
+
/** Accepted with no required-field check. The derived `social_wall_provider` and `social_wall_widget_id` are returned alongside it although this schema does not declare them. */
|
|
1438
1703
|
social_wall_url?: string;
|
|
1704
|
+
/** Which renderer the screen uses. When omitted at create it is DERIVED from whichever content field you sent, in this fixed order: `images`, `youtube_url`, `twitch_url`, `google_slides_url`, `canva_url`, `social_wall_url`, `generic_iframe_url`/`embed_url`, `video_url` (`uploaded`), `local_file_path` (`local`) — and the create is rejected 400 if none resolves. Only `youtube`, `uploaded`, `images` and `twitch` are ever content-analyzed; the four embed types deliberately keep `gemini_analyzed_at` NULL. */
|
|
1439
1705
|
stream_type?: string;
|
|
1706
|
+
/** Required when `stream_type` is `twitch` (400 otherwise). The response also carries the undeclared `twitch_embed_url`, `twitch_content_type` and `twitch_id` companions that the PG reshape returns. */
|
|
1440
1707
|
twitch_url?: string;
|
|
1708
|
+
/** Mapped from `default_streams.updated_at`. Playback bookkeeping writes it — marking a stream current, clearing a stale current flag, or stamping `last_played_at` — so it moves without anyone editing the stream content. */
|
|
1441
1709
|
updatedAt?: string;
|
|
1710
|
+
/** Required when `stream_type` is `uploaded`. The `/default-streams/upload` route sets it for you to the stored object URL of the file you posted (`req.file.location`), so you never send it on that path. */
|
|
1442
1711
|
video_url?: string;
|
|
1712
|
+
/** Required when `stream_type` is `youtube` — the create is rejected 400 (`youtube_url is required for youtube streams`) without it. Null on every other stream type. */
|
|
1443
1713
|
youtube_url?: string;
|
|
1444
1714
|
};
|
|
1445
1715
|
type PartnerDefaultStreamCreate = {
|
|
1716
|
+
/** Accepted with no required-field check, same as `google_slides_url` — a `canva` create with this omitted succeeds and renders nothing. */
|
|
1446
1717
|
canva_url?: string;
|
|
1718
|
+
/** Optional. Falls back to `name` and then to `Background content`. */
|
|
1447
1719
|
description?: string;
|
|
1448
1720
|
/** Optional explicit duration for iframe-based streams */
|
|
1449
1721
|
duration_seconds?: number;
|
|
1722
|
+
/** The URL actually loaded into the iframe. When omitted it is filled from `generic_iframe_url`, so send it only when the embeddable URL differs from the one you want recorded. */
|
|
1450
1723
|
embed_url?: string;
|
|
1724
|
+
/** The iframe source. Sending it also derives `stream_type: generic_iframe` and seeds `embed_url`, so this one field is enough to create an iframe stream. */
|
|
1451
1725
|
generic_iframe_url?: string;
|
|
1726
|
+
/** The `google_slides` type has NO required-field check, so a create naming that type without this URL is accepted and stores null. Send a published-to-web slides URL. */
|
|
1452
1727
|
google_slides_url?: string;
|
|
1728
|
+
/** Only these five keys are kept — every other key you send is silently dropped, a non-object is dropped whole, and `slide_delay_ms`/`refresh_interval_seconds` must be finite numbers or they are dropped individually. On a later update this object is merged over the stored one rather than replacing it. */
|
|
1453
1729
|
iframe_config?: {
|
|
1454
1730
|
allow_flags?: string;
|
|
1455
1731
|
auto_advance?: boolean;
|
|
@@ -1459,12 +1735,19 @@ type PartnerDefaultStreamCreate = {
|
|
|
1459
1735
|
};
|
|
1460
1736
|
/** Seconds per image */
|
|
1461
1737
|
image_duration?: number;
|
|
1738
|
+
/** Required and non-empty for an `images` stream. Accepted as a real array, a JSON-encoded array string, or a comma-separated string; combined with `image_duration` it also derives `duration_seconds` when you do not send one. */
|
|
1462
1739
|
images?: Array<string>;
|
|
1740
|
+
/** Optional. Falls back to `description` and then to the literal `Default Stream`, so the created stream always has one. */
|
|
1463
1741
|
name?: string;
|
|
1742
|
+
/** Accepted with no required-field check — a `social_wall` create with this omitted succeeds and renders nothing. */
|
|
1464
1743
|
social_wall_url?: string;
|
|
1744
|
+
/** Optional — omit it and the server DERIVES it from whichever content field you sent, checking in this order: `images`, `youtube_url`, `twitch_url`, `google_slides_url`, `canva_url`, `social_wall_url`, `generic_iframe_url`/`embed_url`, `video_url` (`uploaded`), `local_file_path` (`local`). If nothing resolves the request is rejected 400 `No supported stream content provided`. */
|
|
1465
1745
|
stream_type?: "local" | "youtube" | "uploaded" | "images" | "twitch" | "google_slides" | "canva" | "social_wall" | "generic_iframe";
|
|
1746
|
+
/** Required when the resolved `stream_type` is `twitch`; the create is rejected 400 without it. */
|
|
1466
1747
|
twitch_url?: string;
|
|
1748
|
+
/** Required when the resolved `stream_type` is `uploaded`. Use `POST .../default-streams/upload` with a multipart file instead if you want us to host the video — that route fills this in for you. */
|
|
1467
1749
|
video_url?: string;
|
|
1750
|
+
/** Required when the resolved `stream_type` is `youtube`; the create is rejected 400 without it. Sending it alone is enough — the type is derived. */
|
|
1468
1751
|
youtube_url?: string;
|
|
1469
1752
|
};
|
|
1470
1753
|
type PartnerDefaultStreamUpdate = PartnerDefaultStreamCreate;
|
|
@@ -1478,6 +1761,7 @@ type PartnerDefaultStreamUpdate = PartnerDefaultStreamCreate;
|
|
|
1478
1761
|
type PartnerDeviceBatchRegistrationInput = {
|
|
1479
1762
|
/** Fleet-wide defaults applied to each device except its identifier fields. */
|
|
1480
1763
|
defaults?: PartnerDeviceRegistrationInput;
|
|
1764
|
+
/** 1 to 100 entries; more than 100 rejects the whole request with `Maximum 100 devices per batch request`. The batch is NOT transactional — entries are registered sequentially and independently, so the call can return 2xx with some entries reported under `errors[]` while the rest committed. An entry whose `device_id` already exists is an update, counted under `summary.updated` rather than `summary.created`. */
|
|
1481
1765
|
devices: Array<PartnerDeviceRegistrationInput>;
|
|
1482
1766
|
};
|
|
1483
1767
|
/**
|
|
@@ -1489,32 +1773,50 @@ type PartnerDeviceBatchRegistrationInput = {
|
|
|
1489
1773
|
*
|
|
1490
1774
|
*/
|
|
1491
1775
|
type PartnerDeviceHeartbeatBody = {
|
|
1776
|
+
/** Legacy flat advertising identifier, superseded by the `advertising_identity` envelope. It is only honoured when no canonical envelope is present AND it passes a plausibility check; a value that fails the check is dropped without an error, and it never reaches the device record through the generic device snapshot — identity has one dedicated persistence rail. */
|
|
1492
1777
|
advertisingId?: string | null;
|
|
1778
|
+
/** Type of the legacy flat `advertisingId`. It is retained only when `advertisingId` itself survives the plausibility check — otherwise it is nulled with it — and the canonical `advertising_identity.id_type` enum is wider (`gaid`, `aaid`, `afai`, `rida`, `tifa`, `idfa`) than the two values allowed here. */
|
|
1493
1779
|
advertisingIdType?: "gaid" | "idfa" | null | null;
|
|
1494
1780
|
advertising_identity?: AdvertisingIdentity;
|
|
1781
|
+
/** Number of distinct BLE devices seen in the scan window, BEFORE the 50-entry cap on `nearbyBleDevices` — so it can legitimately exceed the array length (the on-device scanner tracks up to 100 groups). Omit it and the server substitutes the array length. Both this and the snake_case `ble_device_count` are accepted. */
|
|
1495
1782
|
bleDeviceCount?: number;
|
|
1496
1783
|
capabilities?: DeviceCapabilityPayload;
|
|
1784
|
+
/** mDNS/Bonjour service instances found on the LAN, capped at 50. Accepted by the schema but NOT forwarded by the partner heartbeat handler — that controller contains no reference to wifi/mdns/ssdp/http-probe fields at all and passes only the BLE block to the ingest pipeline. The shipped Android agent sends these to the FIRST-PARTY heartbeat instead, in snake_case (`discovered_network_devices`). */
|
|
1497
1785
|
discoveredNetworkDevices?: Array<MdnsNetworkDevice> | null;
|
|
1786
|
+
/** Up to 16 audience identities (hashed email / UID2) keyed by `(id_type, source)`. Rejection is all-or-nothing — one bad element or a 17th entry drops the whole array — and the row-level upsert additionally refuses any envelope whose `observed_at` is older than the stored one, so a replayed batch is a silent no-op. */
|
|
1498
1787
|
extended_identities?: Array<ExtendedIdentity>;
|
|
1788
|
+
/** The agent's stable device fingerprint. On this endpoint it is inert: the path parameter `deviceId` is the sole binding boundary, and the partner heartbeat controller never reads `fingerprint` from the body — it uses the fingerprint of the device the path already resolved. */
|
|
1499
1789
|
fingerprint?: string;
|
|
1790
|
+
/** Hash of the LAN default-gateway IP — the venue-network join key, and the seed the HTTP probe adds to its mDNS-derived target list. Accepted by the schema but NOT forwarded by the partner heartbeat handler — that controller contains no reference to wifi/mdns/ssdp/http-probe fields at all and passes only the BLE block to the ingest pipeline. The shipped Android agent sends these to the FIRST-PARTY heartbeat instead, in snake_case (`gateway_ip_hash`). */
|
|
1500
1791
|
gatewayIpHash?: string | null;
|
|
1792
|
+
/** Results of HTTP HEAD probes against at most 10 hosts — the mDNS hosts found in the same cycle plus the Wi-Fi gateway, port 80 only. Accepted by the schema but NOT forwarded by the partner heartbeat handler — that controller contains no reference to wifi/mdns/ssdp/http-probe fields at all and passes only the BLE block to the ingest pipeline. The shipped Android agent sends these to the FIRST-PARTY heartbeat instead, in snake_case (`http_probes`). */
|
|
1501
1793
|
httpProbes?: Array<HttpProbeResult> | null;
|
|
1794
|
+
/** Legacy flat LAT flag. `true` alone is enough to clear a previously stored identifier — the server synthesises a `limited` identity envelope from it even with no `advertisingId` present — so do not send `true` speculatively. */
|
|
1502
1795
|
limitAdTracking?: boolean | null;
|
|
1796
|
+
/** Alternate namespace for the same telemetry keys as `telemetry`, merged UNDER it (`{...metadata, ...telemetry}`), so `telemetry` wins on a collision. This is where the Android agents actually put their telemetry — `uptimeSeconds`, `agentType`, `agentVersion`, screen geometry, `sensingMode` — and where the per-heartbeat CPU/memory/thermal gauges are merged in. */
|
|
1503
1797
|
metadata?: Record<string, unknown>;
|
|
1504
1798
|
/** Capped at 50 devices (RSSI-sorted, strongest first). */
|
|
1505
1799
|
nearbyBleDevices?: Array<BleScanResultData> | null;
|
|
1800
|
+
/** Wi-Fi scan results, capped at 50 with the joined AP pinned first. Accepted by the schema but NOT forwarded by the partner heartbeat handler — that controller contains no reference to wifi/mdns/ssdp/http-probe fields at all and passes only the BLE block to the ingest pipeline. The shipped Android agent sends these to the FIRST-PARTY heartbeat instead, in snake_case (`nearby_wifi_networks`, whose inner keys are `raw_bssid`/`signal_dbm`/`frequency_mhz` — note `signal_dbm`, not a snake_case form of `signalStrengthDbm`). */
|
|
1506
1801
|
nearbyWifiNetworks?: Array<WifiScanResult> | null;
|
|
1802
|
+
/** Count of mDNS instances discovered, before the 50-entry cap (the Rust scanner itself keeps at most 30). Accepted by the schema but NOT forwarded by the partner heartbeat handler — that controller contains no reference to wifi/mdns/ssdp/http-probe fields at all and passes only the BLE block to the ingest pipeline. The shipped Android agent sends these to the FIRST-PARTY heartbeat instead, in snake_case (`network_device_count`). */
|
|
1507
1803
|
networkDeviceCount?: number;
|
|
1508
1804
|
/** Heartbeat schema version (1=baseline, 2=nativeSensorSnapshot) */
|
|
1509
1805
|
schemaVersion?: number;
|
|
1806
|
+
/** The screen the agent believes it is bound to. Also inert on this endpoint — the server resolves the screen from the device record, never from this value; correct a wrong binding through registration or the socket heartbeat, which returns an authoritative screen id. */
|
|
1510
1807
|
screenId?: string | null;
|
|
1511
1808
|
/** Per-source skip-reason counter map. Shape `{source: {reason: count}}`. */
|
|
1512
1809
|
skipReasonCounts?: Record<string, Record<string, number>>;
|
|
1810
|
+
/** UPnP/SSDP responders found by one `M-SEARCH` with `ST: ssdp:all`, deduped by LOCATION and capped at 50. Accepted by the schema but NOT forwarded by the partner heartbeat handler — that controller contains no reference to wifi/mdns/ssdp/http-probe fields at all and passes only the BLE block to the ingest pipeline. The shipped Android agent sends these to the FIRST-PARTY heartbeat instead, in snake_case (`ssdp_devices`). */
|
|
1513
1811
|
ssdpDevices?: Array<SsdpDeviceInfo> | null;
|
|
1812
|
+
/** Agent lifecycle word — the Android agents hardcode `"active"` on every heartbeat, so it carries no information. The partner heartbeat controller does not read it; online/standby state is derived from `telemetry.powerState`, not from this field. */
|
|
1514
1813
|
status?: string;
|
|
1814
|
+
/** Hash of the joined access point's BSSID. This field is accepted by the schema but NOT forwarded by the partner heartbeat handler — that controller contains no reference to wifi/mdns/ssdp/http-probe fields at all and passes only the BLE block to the ingest pipeline. The shipped Android agent sends these to the FIRST-PARTY heartbeat instead, in snake_case (`wifi_bssid_hash`). */
|
|
1515
1815
|
wifiBssidHash?: string | null;
|
|
1516
1816
|
wifiEnvironment?: WifiEnvironmentSnapshot;
|
|
1817
|
+
/** Count of distinct access points seen, before the 50-entry cap on `nearbyWifiNetworks`. Accepted by the schema but NOT forwarded by the partner heartbeat handler — that controller contains no reference to wifi/mdns/ssdp/http-probe fields at all and passes only the BLE block to the ingest pipeline. The shipped Android agent sends these to the FIRST-PARTY heartbeat instead, in snake_case (`wifi_network_count`). */
|
|
1517
1818
|
wifiNetworkCount?: number;
|
|
1819
|
+
/** Hash of the joined network's SSID. Same as `wifiBssidHash`: accepted by the schema but NOT forwarded by the partner heartbeat handler — that controller contains no reference to wifi/mdns/ssdp/http-probe fields at all and passes only the BLE block to the ingest pipeline. The shipped Android agent sends these to the FIRST-PARTY heartbeat instead, in snake_case (`wifi_ssid_hash`). */
|
|
1518
1820
|
wifiSsidHash?: string | null;
|
|
1519
1821
|
[key: string]: unknown;
|
|
1520
1822
|
};
|
|
@@ -1529,6 +1831,7 @@ type PartnerDeviceRegistrationInput = {
|
|
|
1529
1831
|
device_id?: string;
|
|
1530
1832
|
/** Legacy SDK alias for the device identifier, retained for backwards compatibility. */
|
|
1531
1833
|
device_name?: string;
|
|
1834
|
+
/** One of `mobile`, `tablet`, `tv`, `connected_tv`, `digital_signage`, `kiosk`, `vending_machine`, `display`, `other`. It is stored as the OpenRTB `devicetype` integer, so the mapping is many-to-one and an unrecognised value is silently ACCEPTED as `digital_signage` rather than rejected — read `device_type` back off the response to see what it collapsed to. */
|
|
1532
1835
|
device_type?: string;
|
|
1533
1836
|
/** Display specification (pixel resolution and/or physical size). */
|
|
1534
1837
|
display?: Record<string, unknown>;
|
|
@@ -1546,34 +1849,48 @@ type PartnerDeviceRegistrationInput = {
|
|
|
1546
1849
|
type PartnerHeartbeatCommand = {
|
|
1547
1850
|
/** Command normalized for partner SDK compatibility. */
|
|
1548
1851
|
command?: string;
|
|
1852
|
+
/** Opaque command identifier — echo it verbatim to `POST /device/{deviceId}/command/{commandId}/ack`. The ack is ownership-checked against the device that received it, so acknowledging another device's command is refused with 403. */
|
|
1549
1853
|
id?: string;
|
|
1854
|
+
/** Command arguments, shape determined by `command` (for example a brightness level or a target URL). Always an object — commands that take no arguments carry `{}`, never null. */
|
|
1550
1855
|
payload?: Record<string, unknown>;
|
|
1856
|
+
/** When the command was enqueued, not when it was dispatched to you — the gap is up to one heartbeat interval, and a large gap means the device was silent, not that the server was slow. */
|
|
1551
1857
|
queued_at?: string;
|
|
1552
1858
|
};
|
|
1553
1859
|
type PartnerHeartbeatData = {
|
|
1554
1860
|
ad_delivery_profile?: PartnerAdDeliveryProfile;
|
|
1861
|
+
/** Pending remote commands, at most 5 per heartbeat, drained from the PG `device_commands` store. Each is marked `sent` as it is handed to you, so it appears in exactly ONE heartbeat response and is not re-delivered — acknowledge every entry via the ack endpoint, and dedupe on `id` because delivery is at-most-once, not exactly-once. */
|
|
1555
1862
|
commands?: Array<PartnerHeartbeatCommand>;
|
|
1863
|
+
/** Server-clock timestamp stamped when this heartbeat was processed — the same instant used for the liveness write, not a device-supplied time and not the time of a previous heartbeat. */
|
|
1556
1864
|
last_seen_at?: string;
|
|
1865
|
+
/** Liveness verdict for the screen. In practice it is `online`, or `standby` when the heartbeat reported `power_state` of `off`/`standby` — `standby` is NOT in the declared enum, and `offline` can never be returned here because receiving this response is itself proof the device reached us. It falls back to `online` when the device is not yet linked to a screen. */
|
|
1557
1866
|
status?: "online" | "offline";
|
|
1558
1867
|
};
|
|
1559
1868
|
type PartnerHeartbeatMeasurementMetadata = {
|
|
1869
|
+
/** Whole seconds since `receivedAt`, computed when the response was built and floored at 0 — the same number as the sibling `stalenessSeconds`. It measures the age of the HEARTBEAT, not of the cache entry; use `cachedAt` for cache age. */
|
|
1560
1870
|
ageSeconds?: number | null;
|
|
1871
|
+
/** When the Redis device twin that served this response was written. It is non-null ONLY when `source` is `redis_twin`, and even then only if the twin carried a `cachedAt`/`observedAt` stamp; the `pg_fallback` and `unknown` paths always report `null`. */
|
|
1561
1872
|
cachedAt?: string | null;
|
|
1873
|
+
/** ISO-8601 server-clock time the underlying heartbeat was received — byte-for-byte the same value as the sibling `lastHeartbeatAt`, not an independent measurement. `null` when no heartbeat has ever been recorded. */
|
|
1562
1874
|
receivedAt?: string | null;
|
|
1875
|
+
/** Which store answered: `redis_twin` (the hot pre-computed device twin — the normal path), `pg_fallback` (the twin was cold or missing, so the persisted last-heartbeat record was used), or `unknown` (no heartbeat has ever been recorded). This is the field that determines which keys `telemetry` can contain, and it is the only signal that the twin was a miss. */
|
|
1563
1876
|
source?: "redis_twin" | "pg_fallback" | "unknown";
|
|
1564
1877
|
[key: string]: unknown;
|
|
1565
1878
|
};
|
|
1566
1879
|
type PartnerHeartbeatQueryData = {
|
|
1567
1880
|
adDeliveryProfile?: PartnerAdDeliveryProfile;
|
|
1881
|
+
/** The device's canonical identifier, which need NOT equal the value you put in the path: the server echoes the device fingerprint, falling back to your registered `external_id` and then to the internal id. Match responses on your own request, not on this field. */
|
|
1568
1882
|
deviceId: string;
|
|
1883
|
+
/** Server-clock time the most recent heartbeat was RECEIVED — never a device-supplied timestamp. `null` means this device has never sent a heartbeat that the server retained; note the value can come from the Redis twin, which expires, so a cold cache falls back to the persisted record rather than reporting null. */
|
|
1569
1884
|
lastHeartbeatAt?: string | null;
|
|
1570
1885
|
measurementMetadata?: PartnerHeartbeatMeasurementMetadata;
|
|
1886
|
+
/** Whole seconds between `lastHeartbeatAt` and the moment this response was built, floored at 0 and rounded — it is computed at read time, so two calls a minute apart return different values for the same heartbeat. `null` when `lastHeartbeatAt` is null. It duplicates `measurementMetadata.ageSeconds` exactly. */
|
|
1571
1887
|
stalenessSeconds?: number | null;
|
|
1572
1888
|
telemetry?: PartnerSafeHeartbeatTelemetry;
|
|
1573
1889
|
[key: string]: unknown;
|
|
1574
1890
|
};
|
|
1575
1891
|
type PartnerHeartbeatQueryResponse = {
|
|
1576
1892
|
data?: PartnerHeartbeatQueryData;
|
|
1893
|
+
/** Always `true` on this 200 response. The 400 (missing `deviceId`), 404 (unknown device), 403 (device belongs to another partner) and 500 paths return `success: false` with a `message` and no `data`. */
|
|
1577
1894
|
success?: boolean;
|
|
1578
1895
|
};
|
|
1579
1896
|
/**
|
|
@@ -1584,17 +1901,24 @@ type PartnerHeartbeatQueryResponse = {
|
|
|
1584
1901
|
*
|
|
1585
1902
|
*/
|
|
1586
1903
|
type PartnerHeartbeatRequest = {
|
|
1904
|
+
/** Highest-precedence source for `last_vast_request_at`, `last_vast_response_at` and `proof_of_play_enabled`. All three are dead on both sides — no agent or SDK in the repo emits this block, and nothing reads the values once stored. */
|
|
1587
1905
|
ad?: {
|
|
1588
1906
|
last_vast_request_at?: string;
|
|
1589
1907
|
last_vast_response_at?: string;
|
|
1590
1908
|
proof_of_play_enabled?: boolean;
|
|
1591
1909
|
};
|
|
1592
1910
|
advertising_identity?: AdvertisingIdentity;
|
|
1911
|
+
/** Flat alias of `telemetry.agentStatus`. No producer uses this spelling; live values arrive camelCase and come from two disjoint vocabularies — `sensing`/`camera_failed`/`starting`/`idle` from the Android sensing SDK, `foreground`/`background` from the browser SDK. */
|
|
1593
1912
|
agent_status?: string;
|
|
1913
|
+
/** Second-precedence source (after `telemetry.agentVersion`) for the Trillboards agent/SDK version — this is the key the Android sensing SDK actually sends on the partner heartbeat. It is kept ONLY if the heartbeat self-identifies as a Trillboards SDK; otherwise the server force-nulls it so a BYO player's app version is never recorded as ours. */
|
|
1594
1914
|
agent_version?: string;
|
|
1915
|
+
/** Android SDK/API level; below 23 forces `vast_fallback` for a native-Android IMA host. The shipped browser SDK derives it from the user-agent token `Android <n>`, which is the platform RELEASE number (Android 9 gives 9), not the API level (28) — so a value under ~23 is more likely a release number than a genuinely old device. */
|
|
1595
1916
|
android_api_level?: number;
|
|
1917
|
+
/** Flat alias of `telemetry.brightness`, on the platform's own 0-100 scale. Only the Tizen and webOS agents emit a brightness at all, and they put it in `metadata`, not here. */
|
|
1596
1918
|
brightness?: number;
|
|
1919
|
+
/** Java 8+ library desugaring in a native-Android IMA host. Nothing in the repo emits it in any casing, so it is absent fleet-wide; it is read only when `ima_integration` is `android_native` and `android_api_level` is 25 or below, where `false` adds a crash-risk note to `ad_delivery_profile.notes`. */
|
|
1597
1920
|
desugaring_enabled?: boolean;
|
|
1921
|
+
/** Hardware self-description. `os`/`osv`/`make`/`model` here rank below `telemetry`/`metadata` and above the flat body keys, while `android_api_level` and `webview_chromium_major` are read from here in snake_case. It also carries the undocumented `native_bridge` object — the Android WebView bridge's ground-truth make/model/OS/advertising identity — and the DOOH taxonomy keys `dth`/`msid`/`venuetype`/`rdid`/`idtype`/`is_lat`. */
|
|
1598
1922
|
device?: {
|
|
1599
1923
|
android_api_level?: number;
|
|
1600
1924
|
make?: string;
|
|
@@ -1603,15 +1927,25 @@ type PartnerHeartbeatRequest = {
|
|
|
1603
1927
|
osv?: string;
|
|
1604
1928
|
webview_chromium_major?: number;
|
|
1605
1929
|
};
|
|
1930
|
+
/** Up to 16 DSP-matchable audience identities (hashed email / UID2). Normalisation is ALL-OR-NOTHING across the array: one malformed element, a duplicate `(id_type, source)` pair beyond the first, or more than 16 entries causes the ENTIRE array to be dropped and the heartbeat still answers 200. Send `[]`-worth of care — a partially valid batch stores nothing. */
|
|
1606
1931
|
extended_identities?: Array<ExtendedIdentity>;
|
|
1932
|
+
/** Flat alias, outranked by `ima.integration`. `html5_webview` marks the heartbeat as coming from a Trillboards SDK (which is what permits `agent_version` to be stored); `android_native` with an API level below 23 forces `vast_fallback`. Unrecognised or absent values become `unknown`, never null. */
|
|
1607
1933
|
ima_integration?: "android_native" | "html5_webview" | "disabled" | "unknown";
|
|
1934
|
+
/** Flat alias with no producer — the browser SDK sends `ima.sdk_version` (from `window.google.ima.VERSION`), which outranks this key and is `null` when IMA failed to load. */
|
|
1608
1935
|
ima_sdk_version?: string;
|
|
1936
|
+
/** Flat alias with no producer — the browser SDK sends `ima.support_reason`, the literal `ok` on success or its IMA load-failure reason. Stored only; the ad-delivery decision never reads it. */
|
|
1609
1937
|
ima_support_reason?: string;
|
|
1938
|
+
/** Flat alias, outranked by `ima.supported` and by `telemetry`/`metadata`. An explicit `false` pins `ad_delivery_profile.mode` to `vast_fallback` and turns `auto_recovery_enabled` off, so the server stops attempting to move the device back onto IMA. */
|
|
1610
1939
|
ima_supported?: boolean;
|
|
1940
|
+
/** Flat alias of `telemetry.inputSource`. Dead on both sides — no producer emits either casing, and the column has no ClickHouse mirror. */
|
|
1611
1941
|
input_source?: string;
|
|
1942
|
+
/** Numeric IMA `AdError` code from the last IMA failure. The browser SDK sends it inside `ima`, which outranks this flat key; it is persisted on the device record and read by nothing. */
|
|
1612
1943
|
last_ima_error_code?: number;
|
|
1944
|
+
/** Numeric VAST error code from the last tag failure (e.g. 303 for an empty wrapper chain), sent inside `ima` by the browser SDK. Persisted only, with no server-side consumer. */
|
|
1613
1945
|
last_vast_error_code?: number;
|
|
1946
|
+
/** Timestamp of the last outbound VAST request. No producer exists anywhere in the repo and nothing reads the stored value, so this field is dead on both ends. */
|
|
1614
1947
|
last_vast_request_at?: string;
|
|
1948
|
+
/** Timestamp of the last VAST response received. Like `last_vast_request_at`, no agent or SDK emits it and nothing consumes it. */
|
|
1615
1949
|
last_vast_response_at?: string;
|
|
1616
1950
|
/** Current GPS fix for a moving screen. Processed only for screens whose `mobility_type` is `mobile` or `semi_mobile` (declare it at registration or via PATCH /device/{deviceId}), OR for any screen still missing coordinates (a one-time self-heal that also kicks off venue classification). Static screens with valid coordinates ignore it. Updates live geo-targeting, geofencing and analytics. Fully optional — omit it on stationary screens. */
|
|
1617
1951
|
location?: {
|
|
@@ -1641,7 +1975,9 @@ type PartnerHeartbeatRequest = {
|
|
|
1641
1975
|
};
|
|
1642
1976
|
/** Alternate namespace for telemetry keys (legacy support). */
|
|
1643
1977
|
metadata?: Record<string, unknown>;
|
|
1978
|
+
/** Flat alias of `telemetry.muted`. The TV agents fall back to `false` here when power state is unreadable while falling back to `null` for `brightness`/`volume`, so a `false` cannot be read as evidence the read succeeded. */
|
|
1644
1979
|
muted?: boolean;
|
|
1980
|
+
/** Connection metrics. The server reads ONLY the camelCase keys shown here; the shipped `trillboards-lite` browser SDK sends this block snake_case (`connection_type`, `effective_type`, `bandwidth_mbps`, `latency_rtt`), so not one key matches and its entire network block is silently discarded on every heartbeat. `bandwidthMbps`/`downlinkMbps` share one slot and `latencyRtt`/`rtt` share another. */
|
|
1645
1981
|
network?: {
|
|
1646
1982
|
bandwidthMbps?: number;
|
|
1647
1983
|
connectionType?: string;
|
|
@@ -1651,9 +1987,13 @@ type PartnerHeartbeatRequest = {
|
|
|
1651
1987
|
rtt?: number;
|
|
1652
1988
|
saveData?: boolean;
|
|
1653
1989
|
};
|
|
1990
|
+
/** Flat alias of `telemetry.networkStrengthDbm`, in dBm (negative). No agent or SDK populates either spelling, so it is absent fleet-wide. */
|
|
1654
1991
|
network_strength_dbm?: number;
|
|
1992
|
+
/** Flat alias of `telemetry.powerState`, used only when the nested value is absent. It coerces to `unknown` rather than failing, and `off`/`standby` downgrade the screen to `standby` so it stops receiving ads. No shipped producer uses this snake_case spelling — every emitter sends the camelCase form. */
|
|
1655
1993
|
power_state?: "on" | "standby" | "off" | "unknown";
|
|
1994
|
+
/** Declares that the player emits proof-of-play. No producer sends it and no server logic reads it — proof-of-play eligibility is decided server-side, not from this flag. */
|
|
1656
1995
|
proof_of_play_enabled?: boolean;
|
|
1996
|
+
/** Lowest-precedence source for the IMA/build fields: `ima_supported`, `ima_integration`, `ima_sdk_version`, `android_api_level` and `webview_chromium_major` are read from here only after `ima.*`, `telemetry`/`metadata` and the flat body keys have all missed. Its `version` sub-key is NOT read at all — the SDK version the server stores comes from body `agent_version`/`sdk_version`. */
|
|
1657
1997
|
sdk?: {
|
|
1658
1998
|
android_api_level?: number;
|
|
1659
1999
|
desugaring_enabled?: boolean;
|
|
@@ -1663,74 +2003,132 @@ type PartnerHeartbeatRequest = {
|
|
|
1663
2003
|
version?: string;
|
|
1664
2004
|
webview_chromium_major?: number;
|
|
1665
2005
|
};
|
|
2006
|
+
/** Third-precedence alias of `agent_version` — same destination and the same BYO-nulling rule. This is what the shipped `trillboards-lite` browser SDK sends, alongside `sdk_type: "lite"`, which is itself one of the signals that classify the heartbeat as ours. */
|
|
1666
2007
|
sdk_version?: string;
|
|
1667
2008
|
telemetry?: PartnerHeartbeatTelemetry;
|
|
2009
|
+
/** Flat alias of `telemetry.temperatureC`, in degrees Celsius from the Android battery thermal sensor. No producer uses this snake_case spelling, and battery-less CTV hardware omits the reading altogether rather than sending 0. */
|
|
1668
2010
|
temperature_c?: number;
|
|
2011
|
+
/** Flat alias of `telemetry.uptimeSeconds`. It measures seconds since DEVICE boot (`SystemClock.elapsedRealtime()/1000` on Android — it survives an agent restart and counts through deep sleep), not agent process uptime. Producers send the camelCase form inside `telemetry`/`metadata`. */
|
|
1669
2012
|
uptime_seconds?: number;
|
|
2013
|
+
/** Flat alias of `telemetry.volume`, on the platform's own scale (0-100 on Tizen/webOS), not a percentage of any documented maximum. Emitted by the Tizen and webOS agents through `metadata` only. */
|
|
1670
2014
|
volume?: number;
|
|
2015
|
+
/** Chromium major version behind the WebView, parsed from the `Chrome/<n>` user-agent token. Below 87 and below 80 each append an advisory note to `ad_delivery_profile.notes`; neither changes the delivery mode. */
|
|
1671
2016
|
webview_chromium_major?: number;
|
|
1672
2017
|
};
|
|
1673
2018
|
type PartnerHeartbeatResponse = {
|
|
1674
2019
|
data?: PartnerHeartbeatData;
|
|
2020
|
+
/** Always `true` on this 200 response. The failure paths — 404 for an unresolvable `deviceId`, 400 for an invalid BLE payload, 500 on an internal error — return `success: false` with a `message` and no `data` key at all. */
|
|
1675
2021
|
success?: boolean;
|
|
1676
2022
|
};
|
|
1677
2023
|
type PartnerHeartbeatTelemetry = {
|
|
2024
|
+
/** A status word whose vocabulary depends on which SDK reports it: the Android sensing SDK derives it from camera health and sends exactly `sensing` (camera open), `camera_failed`, `starting` or `idle`, while the browser SDK sends `foreground`/`background` from `document.hidden`. Those four Android values are also an in-band signal that the heartbeat came from a Trillboards SDK, which is what allows `agentVersion` to be stored. */
|
|
1678
2025
|
agentStatus?: string;
|
|
2026
|
+
/** Version of the TRILLBOARDS agent/SDK only. If the heartbeat does not self-identify as one of ours (`ima.integration=html5_webview`, `sdk_type=lite`, a `capabilities.audienceSensing.cameraHealth` block, or one of the four sensing `agentStatus` values), the server discards what you send and force-nulls both casings, so a BYO player's own app version is never stamped as ours. Resolution order is `telemetry.agentVersion`, then body `agent_version`, then body `sdk_version`. */
|
|
1679
2027
|
agentVersion?: string;
|
|
2028
|
+
/** Android SDK/API level (23 = Marshmallow, 28 = Pie); below 23 it forces `vast_fallback` when `ima_integration` is `android_native`. Beware the unit — the shipped browser SDK derives it from the user-agent token `Android <n>`, which is the platform RELEASE number (Android 9 gives 9), not the API level (28). */
|
|
1680
2029
|
android_api_level?: number;
|
|
2030
|
+
/** Downlink bandwidth in MEGABITS PER SECOND from `navigator.connection.downlink`. It shares one destination with `downlinkMbps` (`bandwidthMbps` wins), then the same two keys inside `network`, so only one value ever survives. */
|
|
1681
2031
|
bandwidthMbps?: number;
|
|
2032
|
+
/** Panel backlight level on the platform's own scale — on Samsung Tizen and LG webOS the 0-100 picture-brightness value, `null` when that picture-control service is unavailable (VIDAA hardcodes `null`, the API does not exist there). Neither partner SDK sends it; the `brightness` seen in the Android agents is a command-ACK detail, not heartbeat telemetry. */
|
|
1682
2033
|
brightness?: number;
|
|
2034
|
+
/** Free-text connection label (`wifi`, `ethernet`, `cellular`…) that becomes `networkMetrics.connectionType`; `networkConnectionType` outranks it and `network.connectionType` is the fallback. It does NOT set the OpenRTB `device.connectiontype` sent in bid requests — that is the separate all-lowercase `connectiontype` key, which is an INTEGER enum (0 unknown, 1 ethernet, 2 wifi, 3 cellular, 4 2G, 5 3G, 6 4G, 7 5G), is not declared in this schema, and only passes because the object allows additional properties. */
|
|
1683
2035
|
connectionType?: string;
|
|
2036
|
+
/** Whether Java 8+ library desugaring is enabled in a native-Android IMA host. No agent or SDK anywhere in the repo emits it, so it is absent fleet-wide; it is consulted only when `ima_integration` is `android_native` and `android_api_level` is 25 or below, where `false` adds a known-crash-risk note to `ad_delivery_profile.notes`. */
|
|
1684
2037
|
desugaring_enabled?: boolean;
|
|
2038
|
+
/** Second-precedence spelling of `bandwidthMbps` (Mbps) filling the same `networkMetrics.bandwidthMbps` slot; ignored whenever `bandwidthMbps` is also present. The npm SDK sends both from the same `connection.downlink` reading. */
|
|
1685
2039
|
downlinkMbps?: number;
|
|
2040
|
+
/** How the player reaches Google IMA. `html5_webview` is hardcoded by both Trillboards browser SDKs and is itself the signal marking the heartbeat as ours (which is what allows `agentVersion` to be stored); `android_native` with `android_api_level` below 23 forces `ad_delivery_profile.mode` to `vast_fallback`. Absent or unrecognised values normalise to `unknown`, never null, and the undocumented nested `ima.integration` key — what the shipped browser SDK actually sends — outranks this flat one. */
|
|
1686
2041
|
ima_integration?: "android_native" | "html5_webview" | "disabled" | "unknown";
|
|
2042
|
+
/** The IMA SDK's own version string. Nothing produces this flat spelling — the shipped browser SDK sends it as `ima.sdk_version`, read from `window.google.ima.VERSION`, which takes precedence — and it is `null` whenever IMA failed to load, so absence usually pairs with `ima_supported: false`. */
|
|
1687
2043
|
ima_sdk_version?: string;
|
|
2044
|
+
/** Free-text explanation for the `ima_supported` verdict — the browser SDK sends the literal `ok` when IMA loaded and its load-failure reason otherwise, under `ima.support_reason`, which outranks this flat spelling (nothing produces the flat one). Stored only: the ad-delivery decision never reads it. */
|
|
1688
2045
|
ima_support_reason?: string;
|
|
2046
|
+
/** Whether the IMA SDK is usable on this player. An explicit `false` is a hard switch, not a hint: it pins `ad_delivery_profile.mode` to `vast_fallback` AND sets `auto_recovery_enabled: false`, so the server stops trying to move the device back onto IMA. The browser SDKs compute it as `Boolean(window.google.ima)` / an IMA load-failure flag. */
|
|
1689
2047
|
ima_supported?: boolean;
|
|
2048
|
+
/** Free-form active-input label (e.g. an HDMI port name), stored verbatim on the device record. It has NO producer: no agent or SDK in the repo emits either casing, the only other references are Flutter UI localisation strings, and the schema sidecar marks the column PG-only with no ClickHouse mirror. Treat it as permanently absent. */
|
|
1690
2049
|
inputSource?: string;
|
|
2050
|
+
/** Numeric IMA `AdError` code from the most recent IMA failure, cached client-side between heartbeats. The shipped browser SDK sends it inside `ima`, which outranks this flat key. Persisted on the device record only — no server logic reads it. */
|
|
1691
2051
|
last_ima_error_code?: number;
|
|
2052
|
+
/** Numeric VAST error code from the most recent tag failure (e.g. 303 for a wrapper chain that resolved to nothing), cached client-side and sent inside `ima` by the browser SDK. Persisted only, with no server-side consumer. */
|
|
1692
2053
|
last_vast_error_code?: number;
|
|
2054
|
+
/** Timestamp of the player's last outbound VAST request. It has NO producer: a repo-wide grep finds only the server-side reader and the generated API client, so it is absent on every live device, and nothing downstream consumes the stored value. */
|
|
1693
2055
|
last_vast_request_at?: string;
|
|
2056
|
+
/** Timestamp of the last VAST response the player received. Like `last_vast_request_at` it has no producer in any agent or SDK, and the persisted value has no consumer. */
|
|
1694
2057
|
last_vast_response_at?: string;
|
|
2058
|
+
/** Round-trip time in MILLISECONDS from `navigator.connection.rtt`. Shares one destination with `rtt` (`latencyRtt` wins); the read-side schema bounds the stored value to 0-60000. */
|
|
1695
2059
|
latencyRtt?: number;
|
|
2060
|
+
/** Device manufacturer. Written to the device column but not authoritative — every heartbeat runs a device-identity fusion pass that can replace `make`/`model`/`os` from the user agent and 51Degrees specs, and the corrected value is what is persisted and read back. */
|
|
1696
2061
|
make?: string;
|
|
2062
|
+
/** Device model, subject to the same identity-fusion overwrite as `make`. The browser SDK has no real model string and sends the full user agent in this field instead. */
|
|
1697
2063
|
model?: string;
|
|
2064
|
+
/** Audio mute state. Note the asymmetry with `brightness`/`volume`: when the Tizen/webOS agent cannot read power state it still sends `false` here while sending `null` for the other two, so `false` does not distinguish "not muted" from "could not be read". The `muted` the browser SDK sends elsewhere belongs to the ad request, not the heartbeat. */
|
|
1698
2065
|
muted?: boolean;
|
|
2066
|
+
/** Highest-precedence spelling of the free-text connection label that becomes `networkMetrics.connectionType`. No agent or SDK emits it, so in practice the value comes from `connectionType` or `network.connectionType`. Sending any of the six network keys makes the server stamp `networkMetrics.measuredAt` and increment `networkMetrics.measurementCount`. */
|
|
1699
2067
|
networkConnectionType?: string;
|
|
2068
|
+
/** Highest-precedence spelling of the Network Information API `effectiveType` bucket (`slow-2g`/`2g`/`3g`/`4g`) — a round-trip-time and downlink classification, not the physical bearer. No producer emits this spelling; live values arrive as `effectiveType` or `network.effectiveType`. */
|
|
1700
2069
|
networkEffectiveType?: string;
|
|
2070
|
+
/** Wi-Fi/cellular RSSI in dBm (negative; the read-side schema bounds it to -120..0). No agent or SDK populates it — the field exists on the Kotlin model and the only value in the tree is a hardcoded test literal — so it is absent fleet-wide. Per-AP signal is instead carried by `nearbyWifiNetworks[].signalStrengthDbm` and `wifiEnvironment.connectedSignalDbm`. */
|
|
1701
2071
|
networkStrengthDbm?: number;
|
|
2072
|
+
/** Operating-system family, written straight to the device column `os` — the Android agents send the full `"Android <release>"` string, not a bare family name. It is an input to the identity-fusion pass that runs on every heartbeat, so what the read endpoint later returns may be a corrected value rather than what you sent. */
|
|
1702
2073
|
os?: string;
|
|
2074
|
+
/** Lower-precedence alias of `osv`: both resolve to the same normalized value and the same `osv` device column, with `osv` winning when both are present. */
|
|
1703
2075
|
osVersion?: string;
|
|
2076
|
+
/** OS version string. `osv` and `osVersion` are two spellings of ONE value — the normalizer takes `osv` first and falls back to `osVersion`, and both land in the single device column `osv`, so sending different values in the two keys silently keeps only `osv`. */
|
|
1704
2077
|
osv?: string;
|
|
2078
|
+
/** Display power as the device reports it. Never null and never rejected: any unrecognised or absent value is coerced to `unknown`, and `off`/`standby` make the server record the screen `standby` rather than `online`, which stops ad delivery. Producers use this camelCase spelling only — the browser SDK derives it from `document.hidden` (`standby` when hidden, else `on`), the Fire TV / Android TV agents send their `PowerController` state; the flat `power_state` alias has no producer at all. */
|
|
1705
2079
|
powerState?: "on" | "standby" | "off" | "unknown";
|
|
2080
|
+
/** Whether the player is configured to emit proof-of-play. No agent or SDK sends it, so it is absent fleet-wide, and nothing reads the stored value — actual proof-of-play eligibility is decided server-side, never from this flag. */
|
|
1706
2081
|
proof_of_play_enabled?: boolean;
|
|
2082
|
+
/** Second-precedence spelling of `latencyRtt` (milliseconds), ignored when `latencyRtt` is also present. */
|
|
1707
2083
|
rtt?: number;
|
|
2084
|
+
/** The browser's Data Saver preference (`navigator.connection.saveData`). Accepted as a boolean or the strings `"true"`/`"false"`; anything else becomes `null` rather than `false`. */
|
|
1708
2085
|
saveData?: boolean;
|
|
2086
|
+
/** Degrees Celsius from the BATTERY thermal sensor — the only real °C reading a non-system Android app can obtain. Battery-less CTV hardware (Fire TV sticks, TV boxes) omits the key entirely rather than sending 0, so absence means "no battery sensor", not "cool"; the derived ClickHouse `cpu_temp_c` was measured above zero on ZERO rows. */
|
|
1709
2087
|
temperatureC?: number;
|
|
2088
|
+
/** Seconds since the DEVICE booted, not since the agent process started: the Android producers send `SystemClock.elapsedRealtime() / 1000`, which keeps counting through deep sleep and resets only on reboot, so an agent crash-loop leaves this climbing. It is also the sole camelCase survivor in the ClickHouse heartbeat blob — every other gauge there is snake_case. */
|
|
1710
2089
|
uptimeSeconds?: number;
|
|
2090
|
+
/** Speaker volume on the platform's own scale — 0-100 on Tizen (`tizen.tvaudiocontrol.getVolume()`) and webOS, `null` when the audio-control service is unavailable or (on VIDAA) does not exist. It is not normalised to a percentage of any maximum, and neither partner SDK sends it. */
|
|
1711
2091
|
volume?: number;
|
|
2092
|
+
/** Major version of the Chromium engine backing the WebView, parsed from the `Chrome/<n>` user-agent token by both browser SDKs. Below 87 adds a content-restoration warning to `ad_delivery_profile.notes` and below 80 an autoplay/media warning; neither changes the delivery mode. */
|
|
1712
2093
|
webview_chromium_major?: number;
|
|
1713
2094
|
[key: string]: unknown;
|
|
1714
2095
|
};
|
|
1715
2096
|
type PartnerLBarContent = {
|
|
2097
|
+
/** The venue name rendered in the L-bar; empty string when unset. One of only three scalar keys (`enabled`, `business_name`, `current_promotion`) the PUT copies straight across — every other key here is an object that gets shallow-merged. */
|
|
1716
2098
|
business_name?: string;
|
|
2099
|
+
/** Holds `phone`, `email`, `website`, `address` and `wifi_password`, all free text and all empty-string when unset, shallow-merged on PUT. `wifi_password` is rendered on the screen, so treat it as public. */
|
|
1717
2100
|
contact_info?: Record<string, unknown>;
|
|
2101
|
+
/** The promo line shown in the L-bar. It is also writable through the much lighter `PUT .../l-bar/live-updates` endpoint as its `promotion` field, which is the path to use for frequent changes. */
|
|
1718
2102
|
current_promotion?: string;
|
|
2103
|
+
/** Free-text lines rotated in the L-bar. Unlike every nested object above, this is REPLACED wholesale whenever it is present in the body, so send the complete list each time. */
|
|
1719
2104
|
custom_messages?: Array<string>;
|
|
2105
|
+
/** The L-bar's own cosmetics — NOT the screen display-settings resource. Holds `background_color` (`#000000`), `text_color` (`#FFFFFF`), `accent_color` (`#4A90E2`), the widget toggles `show_weather`/`show_time`/`show_date`/`show_store_hours` (default true) and `show_contact_info`/`show_social_media`/`show_sports_scores`/`show_stock_market` (default false), plus `selected_sports_leagues`. Shallow-merged on PUT. */
|
|
1720
2106
|
display_settings?: Record<string, unknown>;
|
|
2107
|
+
/** Whether the L-bar content region is drawn; defaults `false`. It is COUPLED to `display_preferences.display_mode` on the PUT — sending `display_mode: "fullscreen"` forces this to `false`, and `"l-bar"` forces it to `true` unless you send an explicit boolean in the same request. */
|
|
1721
2108
|
enabled?: boolean;
|
|
2109
|
+
/** Exactly three sub-objects are merged — `queue_management` (`enabled`, `current_number`, `queue_message`, default `Now Serving`), `wait_time` (`enabled`, `current_wait_minutes`, `wait_message`, default `Current Wait Time`) and `custom_status` (`enabled`, `status_message`); any other key nested here is silently dropped. The live-updates endpoint sets `enabled` for you as a side effect of writing a value, and clears `custom_status.enabled` when you send an empty message. */
|
|
1722
2110
|
live_updates?: Record<string, unknown>;
|
|
2111
|
+
/** Holds `facebook`, `instagram`, `twitter` and `google_reviews`, free text and empty-string when unset, shallow-merged on PUT. */
|
|
1723
2112
|
social_media?: Record<string, unknown>;
|
|
2113
|
+
/** Free-text opening hours: one string per lowercase day name (`monday`..`sunday`) plus a `timezone` defaulting to `America/New_York`. The strings are displayed verbatim — nothing parses them into open/close times — and the PUT shallow-merges them, so you can send a single day. */
|
|
1724
2114
|
store_hours?: Record<string, unknown>;
|
|
1725
2115
|
};
|
|
1726
2116
|
type PartnerLBarDisplayPreferences = {
|
|
2117
|
+
/** Which surfaces ads may occupy; `null` by default, in which case it is derived from `display_mode` + `l_bar_ads_enabled`. Setting `lbar` also flips `display_mode` to `l-bar` when the same request does not set it, and narrows the fullscreen surface to `self_promo` only — paid and programmatic demand stops running full screen. */
|
|
1727
2118
|
ad_surface_mode?: "fullscreen" | "lbar" | "pip" | "all";
|
|
2119
|
+
/** Default `fullscreen`. Writing it has side effects: `fullscreen` also forces `l_bar_ads_enabled` and `l_bar_content.enabled` to `false`, and `l-bar` forces both to `true` unless the same request sets them explicitly. A change also emits a `refresh` control event so the kiosk reloads and swaps surfaces. */
|
|
1728
2120
|
display_mode?: "fullscreen" | "l-bar";
|
|
2121
|
+
/** Seconds one L-bar ad is held; default 8, accepted range 3-30. It must be strictly LESS than `l_bar_ad_interval`, and the check falls back to the stored interval when this request does not send one. */
|
|
1729
2122
|
l_bar_ad_duration?: number;
|
|
2123
|
+
/** Seconds between L-bar ad plays; default 25, accepted range 10-120. It must be strictly GREATER than `l_bar_ad_duration`, and the pair is cross-checked against the STORED values, so lowering this alone can fail on a duration set by an earlier request. */
|
|
1730
2124
|
l_bar_ad_interval?: number;
|
|
2125
|
+
/** Whether ads may fill the L-bar slots. When it has never been set it is DERIVED as `display_mode === "l-bar"`, so it reads `true` on an L-bar screen and `false` on a fullscreen one. Accepts a boolean or the strings `"true"`/`"false"`; anything else is 400. */
|
|
1731
2126
|
l_bar_ads_enabled?: boolean;
|
|
2127
|
+
/** Which edge the L-bar occupies; default `bottom`. It decides ad-slot geometry — `bottom` lays slots out horizontally and `right` vertically — and the slot COUNT is derived from it, so switching sides changes how many `lbar_slot_*` surfaces exist. This endpoint does not validate the value against any enum (the display-settings resource's `l_bar.position` does, and accepts `top`/`bottom`/`left`/`right`). */
|
|
1732
2128
|
l_bar_position?: "bottom" | "right";
|
|
2129
|
+
/** Share of the screen the L-bar occupies, as a PERCENT; default 20. This endpoint accepts it with no range check at all, while the equivalent `l_bar.size_percent` on the display-settings resource clamps to 10-50. It is also not what sizes ad slots — `buildAdSurfaces` uses a fixed 25%. */
|
|
1733
2130
|
l_bar_size?: number;
|
|
2131
|
+
/** Picture-in-picture ad window: `enabled` (default false), `position` (default `bottom-right`), `size_ratio` as a FRACTION of the screen 0.15-0.45 (default 0.3), `aspect_ratio` as width/height 0.5-3 (default 16/9), and `margin` in PIXELS 8-64 (default 24). A PiP surface is only produced when `ad_surface_mode` is `pip` or `all` AND `enabled` is true. */
|
|
1734
2132
|
pip_settings?: {
|
|
1735
2133
|
aspect_ratio?: number;
|
|
1736
2134
|
enabled?: boolean;
|
|
@@ -1740,9 +2138,11 @@ type PartnerLBarDisplayPreferences = {
|
|
|
1740
2138
|
};
|
|
1741
2139
|
};
|
|
1742
2140
|
type PartnerLBarSettings = {
|
|
2141
|
+
/** The resolved renderable ad surfaces. Two caveats: it is present ONLY on the PUT response (the GET does not compute it), and despite the declared `object` type it is an ARRAY — one entry per surface (`fullscreen`, `lbar_slot_1..N`, `pip`) carrying `id`, `surface_type`, `enabled`, `display_area_ratio`, `dimensions` and `allowed_ad_types`. */
|
|
1743
2142
|
ad_surfaces?: Record<string, unknown>;
|
|
1744
2143
|
display_preferences?: PartnerLBarDisplayPreferences;
|
|
1745
2144
|
l_bar_content?: PartnerLBarContent;
|
|
2145
|
+
/** The screen's resolved pacing contract, present only on the PUT response. `policy_version` is `v1` and every interval is in MILLISECONDS (`paid_interval_ms`, `self_promo_interval_ms`, `image_ad_total_ms`, `image_ad_min_ms`); it also carries `max_paid_share` (null unless a loop share cap is enabled), `surface_policy`, `schedule_policy`, `pip_settings` and `lbar_settings`. */
|
|
1746
2146
|
playback_policy?: Record<string, unknown>;
|
|
1747
2147
|
};
|
|
1748
2148
|
/**
|
|
@@ -1760,6 +2160,7 @@ type PartnerMonetizationRequirements = {
|
|
|
1760
2160
|
eventually_due?: Array<PartnerRequirement>;
|
|
1761
2161
|
};
|
|
1762
2162
|
type PartnerNetworkConfig = {
|
|
2163
|
+
/** Partner-level ad pacing and identity controls applied to every screen you own; on the `/ads` path they OUTRANK the screen's own `ad_interval_seconds` and the partner's `sdk_config.max_ads_per_hour`. A PATCH merges key-by-key and rejects unknown keys with a 400, and sending `null` for `identity_mode` or `self_promo_priority_mode` DELETES the key rather than storing null, so those two come back absent rather than as `null`. */
|
|
1763
2164
|
ad_delivery?: {
|
|
1764
2165
|
/** `/ads` response cache lifetime. Zero requires a fresh response for every request. */
|
|
1765
2166
|
cache_max_age_seconds?: number;
|
|
@@ -1791,6 +2192,7 @@ type PartnerNetworkConfig = {
|
|
|
1791
2192
|
};
|
|
1792
2193
|
/** Default content preferences inherited by newly provisioned screens. */
|
|
1793
2194
|
default_content_preferences?: Record<string, unknown>;
|
|
2195
|
+
/** Partner-level (fleet-wide) demand-source ordering; there is no per-screen override for these keys. A PATCH merges key-by-key into what is stored, and any key other than `source_order`/`total_timeout_ms` is rejected with a 400 naming it. The GET omits this object entirely when nothing has been stored, so absence means "never configured" — it is not a rendering of the effective defaults. */
|
|
1794
2196
|
default_waterfall?: {
|
|
1795
2197
|
/** Preferred opaque source aliases from the `/ads` response. Sources not listed here remain enabled and are appended. Internal demand names and `sort_by: cpm` are rejected. */
|
|
1796
2198
|
source_order?: Array<string>;
|
|
@@ -1883,6 +2285,7 @@ type PartnerRegistrationDisplaySettings = {
|
|
|
1883
2285
|
type PartnerRequirement = {
|
|
1884
2286
|
/** Stable machine code, e.g. location_unresolvable, location_awaiting_device_gps, enrichment_failed, display_size_missing. */
|
|
1885
2287
|
code?: string;
|
|
2288
|
+
/** Built mechanically as `https://api.trillboards.com/docs/monetization-requirements#<code>`, so the anchor always matches `code`. A bare `docs.` subdomain is deliberately NOT used — it resolves to an unrelated portal SPA rather than documentation — and a CI guard blocks reintroducing it. */
|
|
1886
2289
|
doc_url?: string;
|
|
1887
2290
|
/** The request field this concerns (location, venue, display). */
|
|
1888
2291
|
field?: string;
|
|
@@ -1899,52 +2302,90 @@ type PartnerRequirement = {
|
|
|
1899
2302
|
*
|
|
1900
2303
|
*/
|
|
1901
2304
|
type PartnerSafeHeartbeatTelemetry = {
|
|
2305
|
+
/** Agent lifecycle word, with two disjoint vocabularies depending on the reporting SDK (`sensing`/`camera_failed`/`starting`/`idle` from the Android sensing SDK, `foreground`/`background` from the browser SDK). Absent on the primary `redis_twin` path — the device twin has no such field, so it surfaces only on the `pg_fallback` path. */
|
|
1902
2306
|
agentStatus?: string | null;
|
|
2307
|
+
/** Trillboards agent/SDK version. Never present here on either path: the heartbeat writes it to the device record root, not into the last-heartbeat telemetry blob, and the device twin does not carry it either. It is also nulled at write time for any heartbeat that does not self-identify as a Trillboards SDK. */
|
|
1903
2308
|
agentVersion?: string | null;
|
|
2309
|
+
/** Downlink bandwidth in Mbps. Never present on either path: the write path stores it under `networkMetrics.bandwidthMbps`, a sibling of the last-heartbeat blob this endpoint reads, and the device twin has no such field. */
|
|
1904
2310
|
bandwidthMbps?: number | null;
|
|
2311
|
+
/** Panel brightness on the platform's own scale (0-100 on Tizen/webOS, `null` where no picture-control API exists). Absent on the primary `redis_twin` path — the device twin has no such field, so it can only appear on the `pg_fallback` path and only for the TV agents that emit it. */
|
|
1905
2312
|
brightness?: number | null;
|
|
2313
|
+
/** Free-text connection label. Not present on either path — the write path stores it as `networkMetrics.connectionType`, and while the device twin does carry a `connectiontype`, that is the all-lowercase OpenRTB INTEGER enum, a different quantity under a different key. */
|
|
1906
2314
|
connectionType?: string | null;
|
|
2315
|
+
/** Running count of back-to-back ad failures the player has seen, from the heartbeat's `playback.consecutive_failures`; it is what drives the server's stalled-playback recovery. Absent on the primary `redis_twin` path — the device twin has no such field, so it appears only on the `pg_fallback` path. */
|
|
1907
2316
|
consecutiveAdFailures?: number | null;
|
|
2317
|
+
/** Network Information API `effectiveType` bucket (`slow-2g`/`2g`/`3g`/`4g`) — a latency/throughput classification, not the physical bearer. Stored under `networkMetrics.effectiveType` and carried by neither source object, so it never appears here. */
|
|
1908
2318
|
effectiveType?: string | null;
|
|
2319
|
+
/** Round-trip time in MILLISECONDS (bounded 0-60000 by the read-side schema). Like `bandwidthMbps` it is stored under `networkMetrics.*` and is on neither of this endpoint's two source objects. */
|
|
1909
2320
|
latencyRtt?: number | null;
|
|
2321
|
+
/** Device manufacturer, after the identity-fusion correction pass — so it can differ from what the device reported. Present only on the `redis_twin` path: it is a device-record field, and the `pg_fallback` path returns just the last-heartbeat telemetry blob. */
|
|
1910
2322
|
make?: string | null;
|
|
2323
|
+
/** Device model, after identity-fusion correction. Like `make`, it is a twin-only field and is absent whenever `measurementMetadata.source` is `pg_fallback`. */
|
|
1911
2324
|
model?: string | null;
|
|
2325
|
+
/** Mute state. Absent on the primary `redis_twin` path — the device twin has no such field. On the agents that do report it, `false` is also the fallback used when the power state could not be read at all, so it does not prove a successful read. */
|
|
1912
2326
|
muted?: boolean | null;
|
|
2327
|
+
/** RSSI in dBm, bounded -120..0. Doubly absent: no shipped agent produces it in the first place, and absent on the primary `redis_twin` path — the device twin has no such field. */
|
|
1913
2328
|
networkStrengthDbm?: number | null;
|
|
2329
|
+
/** Declared but NEVER POPULATED on either source path: nothing writes an `onlineStatus` into the last-heartbeat telemetry blob and the device twin has no such field. The equivalent state lives on the screen row (`earner_screens.online_status`), which this endpoint does not read — use `stalenessSeconds` to judge liveness instead. */
|
|
1914
2330
|
onlineStatus?: "online" | "offline" | "standby" | "unknown" | null;
|
|
2331
|
+
/** Operating-system family after correction — the Android agents report the full `"Android <release>"` string. Twin-only, so absent on the `pg_fallback` path. */
|
|
1915
2332
|
os?: string | null;
|
|
2333
|
+
/** OS version string after correction; the heartbeat collapses the `osv` and `osVersion` request spellings into this one value. Twin-only, so absent on the `pg_fallback` path. */
|
|
1916
2334
|
osv?: string | null;
|
|
2335
|
+
/** Last reported display power (`on`/`standby`/`off`/`unknown`). Absent on the primary `redis_twin` path — the device twin has no such field, so it appears only when `measurementMetadata.source` is `pg_fallback`, which reads the persisted last-heartbeat telemetry blob. */
|
|
1917
2336
|
powerState?: string | null;
|
|
2337
|
+
/** Degrees Celsius from the Android BATTERY thermal sensor (bounded -50..150 by the read schema). Absent on the primary `redis_twin` path — the device twin has no such field, and battery-less CTV hardware never reports it at all, so absence carries no thermal information. */
|
|
1918
2338
|
temperatureC?: number | null;
|
|
2339
|
+
/** User agent, resolved with a three-tier fallback — the corrected device UA, then the stored device UA, then the `User-Agent` header of the heartbeat request that built the twin — so it can reflect the HTTP client rather than the player. Twin-only. */
|
|
1919
2340
|
ua?: string | null;
|
|
2341
|
+
/** Seconds since the DEVICE booted, not since the agent started — it survives an agent restart and counts through deep sleep. Absent on the primary `redis_twin` path — the device twin has no such field, so it surfaces only on the `pg_fallback` path. */
|
|
1920
2342
|
uptimeSeconds?: number | null;
|
|
2343
|
+
/** The browser `document.visibilityState` of the player page (`visible`/`hidden`), sourced from the heartbeat's `content.visibility_state`. One of only two telemetry-derived fields that survive onto the Redis device twin, so it is the one health signal available on BOTH source paths. */
|
|
1921
2344
|
visibilityState?: string | null;
|
|
2345
|
+
/** Speaker volume on the platform's own scale, not a normalised percentage. Absent on the primary `redis_twin` path — the device twin has no such field; only the Tizen and webOS agents ever supply it. */
|
|
1922
2346
|
volume?: number | null;
|
|
1923
2347
|
[key: string]: unknown;
|
|
1924
2348
|
};
|
|
1925
2349
|
type PartnerSelfPromoAd = {
|
|
2350
|
+
/** The 24-hex advertisement id, minted server-side with `newObjectId()`, and the `ad_id` the schedule and disable endpoints take. Partner self-promo rows carry no `user_mongo_id`, so this id is only resolvable through the partner's own `partner-self-promo` placements — an owner-keyed lookup structurally cannot find it. */
|
|
1926
2351
|
_id?: string;
|
|
2352
|
+
/** Click-through / QR destination; empty string, not null, when it was never set — the PG read coalesces it the same way. */
|
|
1927
2353
|
campaign_link?: string;
|
|
2354
|
+
/** When the advertisement row was written. It is the rotation order for a screen's self-promo pool: the placements read sorts `instant_ad_qr` first, then oldest `created_at` first. */
|
|
1928
2355
|
createdAt?: string;
|
|
2356
|
+
/** Defaults to `name` at create, never null. Note the placements listing trims the embedded advertisement to `_id`, `name`, `video`, `photos`, `status` — this field is absent there. */
|
|
1929
2357
|
description?: string;
|
|
2358
|
+
/** Required at create. It is also copied onto each placement as `advertisement_name`. */
|
|
1930
2359
|
name?: string;
|
|
2360
|
+
/** Image creatives. A JSON create puts `image_url` here as the single element; the multipart route puts the uploaded file here when its MIME type is `image/*`. Empty array for a video-only promo. */
|
|
1931
2361
|
photos?: Array<string>;
|
|
2362
|
+
/** Minted as `approved` — self-promo skips review entirely. The disable endpoint sets it to `deactivated`, and that is the ONLY thing that stops delivery: placements stay `booked` so the disable is idempotent, and every serve-path read has to gate on this field. The listings additionally hide `deleted` and `rejected`. */
|
|
1932
2363
|
status?: string;
|
|
2364
|
+
/** URL of the video creative, or `null` for an image-only promo. This is the field the `trillboards_direct` VAST bridge serves from, which is why the advertisement row is written and awaited BEFORE any placement is created. */
|
|
1933
2365
|
video?: string;
|
|
1934
2366
|
/** Measured length of the `video` creative in whole seconds, probed when the ad is created. This is the real media length and is what the served VAST declares as `<Duration>`. `null` means the media could not be measured, in which case the VAST declares the 30 second ad-slot default instead. */
|
|
1935
2367
|
video_duration_seconds?: number | null;
|
|
1936
2368
|
};
|
|
1937
2369
|
type PartnerSelfPromoCreate = {
|
|
2370
|
+
/** Click-through / QR destination. Stored as an empty string, not null, when omitted. */
|
|
1938
2371
|
campaign_link?: string;
|
|
2372
|
+
/** Optional; defaults to `name` rather than staying null. */
|
|
1939
2373
|
description?: string;
|
|
2374
|
+
/** Last day the promo may serve, in each screen's own timezone. Omitting it leaves the placement OPEN-ENDED (`preserveOpenEnd: true`) — it runs until you disable the ad — and leaves `end_time` null rather than 23:59. */
|
|
1940
2375
|
end_date?: string;
|
|
2376
|
+
/** The image creative, stored as a single-element `photos` array. At least one of `image_url` or `video_url` is required (400 `Provide image_url or video_url`); sending both stores both. */
|
|
1941
2377
|
image_url?: string;
|
|
2378
|
+
/** Required — the create is rejected 400 without it. It is also written onto every placement as `advertisement_name`, so it is what appears in delivery bookkeeping. */
|
|
1942
2379
|
name: string;
|
|
2380
|
+
/** Which of YOUR screens the promo is placed on. Accepts a real array, a JSON-encoded array string, or a comma-separated string; the list is intersected with the partner's own non-deactivated screens so a foreign id is dropped silently, but if you send ids and NONE survive the create fails 404. Omitting it entirely creates the advertisement with no placements, so it serves nowhere until you call the schedule endpoint. */
|
|
1943
2381
|
screen_ids?: Array<string>;
|
|
2382
|
+
/** First day the promo may serve, resolved PER SCREEN in that screen's own timezone. A window that is already open lands the placement `active` and fires an immediate `self_promo_active` push to the screen; a future one lands it `scheduled`. Serving times are fixed at 00:00-23:59 local and are not derived from this value. */
|
|
1944
2383
|
start_date?: string;
|
|
2384
|
+
/** The video creative. Its length is PROBED at create time and returned as `video_duration_seconds`, which is what the served VAST declares as `<Duration>`; a spot longer than the 30-second slot also comes back as a non-fatal entry in the response `warnings` array. The probe never blocks the create — on failure the duration stays null and the VAST falls back to a flat 30s. */
|
|
1945
2385
|
video_url?: string;
|
|
1946
2386
|
};
|
|
1947
2387
|
type PartnerSelfPromoPlacement = {
|
|
2388
|
+
/** One entry per `partner-self-promo` placement on that screen, each carrying `allocation_id`, `status`, `start_date`, `end_date` and a TRIMMED `advertisement` (`_id`, `name`, `video`, `photos`, `status` only — no `description`, `campaign_link` or `createdAt`). The underlying read filters out `deactivated`/`deleted`/`rejected` advertisements, so a disabled promo disappears from here even though its placement row still exists. */
|
|
1948
2389
|
ads?: Array<{
|
|
1949
2390
|
advertisement?: PartnerSelfPromoAd;
|
|
1950
2391
|
allocation_id?: string;
|
|
@@ -1952,17 +2393,24 @@ type PartnerSelfPromoPlacement = {
|
|
|
1952
2393
|
start_date?: string;
|
|
1953
2394
|
status?: string;
|
|
1954
2395
|
}>;
|
|
2396
|
+
/** Trimmed to `_id` and `name` only. EVERY non-deactivated screen the partner owns appears in this listing, including ones with an empty `ads` array — the response is a full screen roster, not just the placed subset. */
|
|
1955
2397
|
screen?: {
|
|
1956
2398
|
_id?: string;
|
|
1957
2399
|
name?: string;
|
|
1958
2400
|
};
|
|
1959
2401
|
};
|
|
1960
2402
|
type PartnerStats = {
|
|
2403
|
+
/** Recomputed by the daily stats cron as the count of the partner's screens in status `approved`/`active` — an APPROVAL count, not a liveness count. The identically-named field on `GET /v1/partner/analytics` is instead derived live from `online_status`, so the two measure different things. */
|
|
1961
2404
|
active_devices?: number;
|
|
2405
|
+
/** ISO timestamp stamped by the partner impression-record endpoints on each recorded impression. Only those endpoints write it, so it does not move for revenue booked through other rails. */
|
|
1962
2406
|
last_impression_at?: string;
|
|
2407
|
+
/** Integer US cents already reserved by an in-flight payout request. Sourced from the typed `partners.pending_payout_cents` column (the JSONB key was dropped) and subtracted from the available balance so repeat payout requests cannot double-spend. */
|
|
1963
2408
|
pending_payout_cents?: number;
|
|
2409
|
+
/** Read from the `partners.stats` JSONB cache, maintained by signed-delta counters on device register/delete. `GET /v1/partner/analytics` publishes the same field name from a LIVE device count instead, so the two can disagree. */
|
|
1964
2410
|
total_devices?: number;
|
|
2411
|
+
/** Incremented per impression by the partner impression-record path, then OVERWRITTEN by the daily reconcile with `SUM(impressions_count)` over `programmatic_earnings_daily` — two different denominators writing one key, so its meaning depends on when in the cron cycle you read it. */
|
|
1965
2412
|
total_impressions?: number;
|
|
2413
|
+
/** Integer US cents. Recomputed daily as `SUM(earner_revenue) * 100` across ALL of the partner's non-demo `programmatic_earnings_daily` rows, with no payable-source filter and no Net-60 maturity gate — so it can exceed `EarningsResponse.total_lifetime_cents`, which applies the payable filter. */
|
|
1966
2414
|
total_revenue_cents?: number;
|
|
1967
2415
|
};
|
|
1968
2416
|
/**
|
|
@@ -2029,10 +2477,15 @@ type PartnerZoneCpmDaily = {
|
|
|
2029
2477
|
zone_label?: string | null;
|
|
2030
2478
|
};
|
|
2031
2479
|
type PayoutResponse = {
|
|
2480
|
+
/** Integer US cents actually reserved: `min(requested, available)`, where available is CONFIRMED earnings minus anything already pending. It must clear the partner's `payout_threshold_cents` (default 2500, i.e. $25.00) or the request is rejected `400`. */
|
|
2032
2481
|
amount_cents?: number;
|
|
2482
|
+
/** Today plus 5 CALENDAR days, computed in the handler with no reference to banking days, the Stripe payout schedule, or whether any transfer exists. It is a display placeholder. */
|
|
2033
2483
|
estimated_arrival?: string;
|
|
2484
|
+
/** Generated in the handler as `po_<epoch_ms>_<random>` and **not persisted anywhere** — no row, no Stripe object. It cannot be used to look the payout up afterwards. */
|
|
2034
2485
|
payout_id?: string;
|
|
2486
|
+
/** Always the literal `processing` on a successful request — the other enum values are never returned by this endpoint. The only side effect is a bump to the partner's `pending_payout_cents` counter; no Stripe transfer is created here. */
|
|
2035
2487
|
status?: "pending" | "processing" | "completed" | "failed";
|
|
2488
|
+
/** **Always `null`.** The handler returns a hardcoded null because it creates no transfer; a real transfer id appears only later on the corresponding `payout` row from `GET /earnings/transactions`. */
|
|
2036
2489
|
stripe_transfer_id?: string | null;
|
|
2037
2490
|
};
|
|
2038
2491
|
type PlaygroundResult = {
|
|
@@ -2072,11 +2525,13 @@ type ProgrammaticSettingsInput = {
|
|
|
2072
2525
|
enabled?: boolean;
|
|
2073
2526
|
/** Minimum CPM in dollars */
|
|
2074
2527
|
floor_price_cpm?: number;
|
|
2528
|
+
/** Accepted by this schema but silently discarded. The write path keeps only an explicit whitelist — `enabled`, `provider`, `vast_tag_url`, `timeout_ms`, `programmatic_allocation_percent`, `floor_price_cpm`, `currency`, `dynamic_floor_pricing`, `blocked_advertisers`, `blocked_iab_categories`, `preferred_deal_ids` — and `google_ima` is not in it. A body containing only this key persists nothing and still returns 200. */
|
|
2075
2529
|
google_ima?: {
|
|
2076
2530
|
ad_tag_url?: string;
|
|
2077
2531
|
enabled?: boolean;
|
|
2078
2532
|
max_duration?: number;
|
|
2079
2533
|
};
|
|
2534
|
+
/** Accepted by this schema but silently discarded — not in the write whitelist, never persisted, and never read back (`header_bidding_settings.prebid` is JSON `null` on every row). A body containing only this key persists nothing and still returns 200. */
|
|
2080
2535
|
prebid?: {
|
|
2081
2536
|
bidders?: Array<{
|
|
2082
2537
|
name?: string;
|
|
@@ -2088,26 +2543,36 @@ type ProgrammaticSettingsInput = {
|
|
|
2088
2543
|
priority?: "direct_first" | "programmatic_first" | "highest_bid";
|
|
2089
2544
|
};
|
|
2090
2545
|
type ProgrammaticSettingsResponse = {
|
|
2546
|
+
/** The master serving gate `earner_screens.programmatic_enabled`, which is `NOT NULL DEFAULT true` — so it reads `true` for a screen nobody has ever configured — and NOT the legacy `header_bidding_settings.enabled` shadow. Setting it false stops programmatic serving for the screen; the write invalidates the serving caches, and a missed invalidation still drains within the 120s hot-cache TTL. */
|
|
2091
2547
|
enabled?: boolean;
|
|
2548
|
+
/** Minimum clearing price in DOLLARS per thousand impressions (not cents), default `0.50`, accepted range 0.01-100, denominated in the screen's `currency`. It is a floor only: the auction clears at the `max()` of this, any floor rule, the dynamic floor and any matched deal floor, so a low value here does not guarantee cheap fill and can never lower a rule-derived floor. */
|
|
2092
2549
|
floor_price_cpm?: number;
|
|
2550
|
+
/** Never emitted. The response formatter builds the entire body and has no `google_ima` key, and the string appears nowhere on this API's request or response surface — there is no producer. Configure the IMA tag through the screen's `vast_tag_url` / VAST waterfall instead. */
|
|
2093
2551
|
google_ima?: {
|
|
2094
2552
|
ad_tag_url?: string;
|
|
2095
2553
|
enabled?: boolean;
|
|
2096
2554
|
max_duration?: number;
|
|
2097
2555
|
};
|
|
2556
|
+
/** Constant discriminator `programmatic_settings` (Stripe-style envelope). It is present on the GET, the PUT and the PATCH alike, so it identifies the resource type and tells you nothing about which verb produced the response. */
|
|
2098
2557
|
object?: string;
|
|
2558
|
+
/** Never emitted. The response formatter has no `prebid` key, and the underlying `header_bidding_settings.prebid` is JSON `null` on every stored row with no typed home — nothing populates it on either side. */
|
|
2099
2559
|
prebid?: {
|
|
2100
2560
|
bidders?: Array<Record<string, unknown>>;
|
|
2101
2561
|
enabled?: boolean;
|
|
2102
2562
|
};
|
|
2563
|
+
/** Never emitted, and not settable. The response formatter has no `priority` key and the field is absent from the write whitelist, so `direct_first` / `programmatic_first` / `highest_bid` can neither be stored through this endpoint nor read back from it. */
|
|
2103
2564
|
priority?: string;
|
|
2565
|
+
/** Server-minted per HTTP request in the form `req_<32 hex>`, and also returned in the `X-Request-Id` response header. A client-supplied `X-Request-Id` is echoed back separately as `X-Client-Request-Id` and never becomes this value. Unrelated to `HeaderBiddingSettings.request_id`, which identifies a VAST waterfall attempt. */
|
|
2104
2566
|
request_id?: string;
|
|
2567
|
+
/** Echoed straight back from the path parameter — it is not re-read from the stored row. Must be a 24-character hex Mongo ObjectId; anything else is rejected before any lookup happens. */
|
|
2105
2568
|
screen_id?: string;
|
|
2569
|
+
/** Never emitted — the response formatter has no `stats` key, so `impressions_today`, `fill_rate_percent` and `avg_cpm` have no producer here; use the partner analytics endpoints for delivery numbers. When you do, note that partner-rail impression counts are delivery accounting, not billing: only `completed_impressions` settles revenue. */
|
|
2106
2570
|
stats?: {
|
|
2107
2571
|
avg_cpm?: number;
|
|
2108
2572
|
fill_rate_percent?: number;
|
|
2109
2573
|
impressions_today?: number;
|
|
2110
2574
|
};
|
|
2575
|
+
/** The SCREEN row's `updated_at`, so any change to the screen moves it — not just a programmatic-settings change. When the row carries no timestamp the server substitutes the current time, so this can never be relied on as 'when these settings last changed' or used as a change-detection cursor. */
|
|
2111
2576
|
updated_at?: string;
|
|
2112
2577
|
};
|
|
2113
2578
|
type RequestMetadataResponse = {
|
|
@@ -2133,12 +2598,15 @@ type RequestMetadataResponse = {
|
|
|
2133
2598
|
type ResolvedContentPolicy = {
|
|
2134
2599
|
/** All content_preferences keys + blocked_iab_categories. */
|
|
2135
2600
|
effective?: Record<string, unknown>;
|
|
2601
|
+
/** Always `resolved_content_policy`. This resource is read-only introspection — a "why is this screen serving what it serves" preview — and unlike the content-preferences response it IS wrapped in `{ success, data }`. */
|
|
2136
2602
|
object?: string;
|
|
2603
|
+
/** Which single layer supplied the whole `effective` object. `source` is `screen` when the screen's OWN policy is explicit — specifically `content_filtering_enabled === true`, or any of the six content lists non-empty, or a non-empty `programmatic_settings.blocked_iab_categories`; the `allow_*` toggles and the two scalars NEVER make it explicit because they always carry a default. Otherwise it is `group` (lowest `group_id` wins, and `group_id` is present only then), then `network` (the partner's `network_config.default_content_preferences`, used only when it has at least one populated key and merged onto the system baseline), then `default`. */
|
|
2137
2604
|
provenance?: {
|
|
2138
2605
|
/** Present only when source=group. */
|
|
2139
2606
|
group_id?: string;
|
|
2140
2607
|
source?: "screen" | "group" | "network" | "default";
|
|
2141
2608
|
};
|
|
2609
|
+
/** The screen whose effective policy this is, echoed from the path parameter as a string. Ownership is checked first, so a screen you do not own returns 404 rather than an empty policy. */
|
|
2142
2610
|
screen_id?: string;
|
|
2143
2611
|
};
|
|
2144
2612
|
/**
|
|
@@ -2147,6 +2615,7 @@ type ResolvedContentPolicy = {
|
|
|
2147
2615
|
*
|
|
2148
2616
|
*/
|
|
2149
2617
|
type ResolvedSensingConfig = {
|
|
2618
|
+
/** The profile scalars actually in force after resolution. When the winning layer is a group, the base profile is hydrated first and only the group's NON-null overrides are laid over it, so this is always a complete view rather than a sparse patch. */
|
|
2150
2619
|
effective?: {
|
|
2151
2620
|
capture_interval_ms?: number | null;
|
|
2152
2621
|
estimated_fps_impact?: number | null;
|
|
@@ -2154,12 +2623,15 @@ type ResolvedSensingConfig = {
|
|
|
2154
2623
|
profile_name?: string | null;
|
|
2155
2624
|
report_interval_ms?: number | null;
|
|
2156
2625
|
};
|
|
2626
|
+
/** Stripe-style type discriminator; always the literal `resolved_sensing_config`, so it is safe to switch on. */
|
|
2157
2627
|
object?: string;
|
|
2628
|
+
/** Which layer won: `screen` (the screen's own active deployment), `group_default` (a group sensing config, with `group_id` naming it), or `default` (nothing bound). Resolution is whole-layer — `effective` never mixes values from two sources. */
|
|
2158
2629
|
provenance?: {
|
|
2159
2630
|
/** Present only when source=group_default. */
|
|
2160
2631
|
group_id?: string;
|
|
2161
2632
|
source?: "screen" | "group_default" | "default";
|
|
2162
2633
|
};
|
|
2634
|
+
/** The screen's Mongo id — the same identifier the route surfaces call `screenMongoId`. Echoes the path parameter. */
|
|
2163
2635
|
screen_id?: string;
|
|
2164
2636
|
};
|
|
2165
2637
|
/**
|
|
@@ -2194,12 +2666,17 @@ type RolePermissions = {
|
|
|
2194
2666
|
type SDKConfig = {
|
|
2195
2667
|
/** Seconds between ads */
|
|
2196
2668
|
ad_interval?: number;
|
|
2669
|
+
/** The only `sdk_config` key that changes what is SERVED: if it contains `image`, the server stops substituting a transcoded MP4 for an image creative on `lite_sdk`/`full_sdk` tiers, because you are declaring the client can render images itself. Frozen at the quick-start default `["video", "image"]` — there is no partner-facing endpoint that writes `sdk_config`. */
|
|
2197
2670
|
allowed_ad_types?: Array<string>;
|
|
2671
|
+
/** Maximum number of ads the `trillboards-connect` client retains for offline playback (`cacheAdsOffline`). Client-side hint only — the server never reads it — and it is frozen at the quick-start default of 10 because no partner-facing endpoint writes `sdk_config`. */
|
|
2198
2672
|
cache_size?: number;
|
|
2199
2673
|
/** Seconds to display image ads */
|
|
2200
2674
|
image_duration?: number;
|
|
2675
|
+
/** Fleet-wide hourly ad cap echoed as `settings.max_ads_per_hour` in the `/ads` response, but only as the FALLBACK behind `network_config.ad_delivery.max_ads_per_hour` — that is the writable control, and this one is frozen at the quick-start default of 20 because no partner-facing endpoint writes `sdk_config`. */
|
|
2201
2676
|
max_ads_per_hour?: number;
|
|
2677
|
+
/** Label the lite SDK paints on the tap-to-continue overlay; when absent the SDK falls back to the literal `TAP TO CONTINUE`, which is also the quick-start default stored for every partner. Frozen — there is no partner-facing endpoint that writes `sdk_config`. */
|
|
2202
2678
|
overlay_button_text?: string;
|
|
2679
|
+
/** Has no reader anywhere in the server or the shipped SDKs — the `settings.sound_enabled` a player actually obeys is resolved per screen from `earner_screens.sound_enabled` / display preferences, not from this partner-level value. Frozen at the quick-start default `false`. */
|
|
2203
2680
|
sound_enabled?: boolean;
|
|
2204
2681
|
};
|
|
2205
2682
|
/**
|
|
@@ -2209,8 +2686,11 @@ type SDKConfig = {
|
|
|
2209
2686
|
*
|
|
2210
2687
|
*/
|
|
2211
2688
|
type Screen = {
|
|
2689
|
+
/** City resolved at registration by reverse/forward geocoding or taken from your own `location.city`, with placeholder strings ("City", "Vending Machine Location") coalesced to null rather than stored. The screens resource reads the partner device endpoints, whose payload carries this as `location.city`. */
|
|
2212
2690
|
city?: string;
|
|
2691
|
+
/** Country of the screen, taken from the enriched screen row with a fallback to the device row. The device row DEFAULTS this to `"US"` when registration could not resolve a country, so it is effectively never null and a `US` value can mean "unresolved". The screens resource reads the partner device endpoints, whose payload carries this as `location.country`. */
|
|
2213
2692
|
country?: string;
|
|
2693
|
+
/** Creation time of the DEVICE row backing the screen, not of the screen record itself — the screens resource is a projection of the partner device endpoints, and the screen can be created later than its device. */
|
|
2214
2694
|
created_at: string;
|
|
2215
2695
|
/** The paired device identifier. */
|
|
2216
2696
|
device_id?: string;
|
|
@@ -2224,23 +2704,28 @@ type Screen = {
|
|
|
2224
2704
|
screen_id: string;
|
|
2225
2705
|
/** Screen lifecycle status (e.g. pending, active, offline, suspended). */
|
|
2226
2706
|
status: string;
|
|
2707
|
+
/** IANA zone, and it is ONLY ever what you supplied as `location.timezone` at registration — nothing derives it from the resolved coordinates, so a large share of screens carry null. The screens resource reads the partner device endpoints, whose payload carries this as `location.timezone`, and it is the one geo field read device-first rather than screen-first. */
|
|
2227
2708
|
timezone?: string;
|
|
2709
|
+
/** `earner_screen_devices.updated_at` of the backing device, which the device-mirror write buffer stamps `NOW()` on every flush including heartbeats. It moves continuously on a live screen, so it cannot be used to detect a configuration change. */
|
|
2228
2710
|
updated_at: string;
|
|
2229
2711
|
/** Granular OpenOOH venue type of the screen's location. */
|
|
2230
2712
|
venue_type?: string;
|
|
2231
2713
|
};
|
|
2232
2714
|
type ScreenAnalyticsResponse = {
|
|
2715
|
+
/** `total` is deduped, GIVT-passed `completed_impressions` over the period. `daily_average` divides by the number of days that RETURNED A ROW, not by the period length, so zero-impression days do not dilute it. `trend_percent` compares the second half of the returned rows against the first half and is `0` whenever the earlier half is zero. */
|
|
2233
2716
|
impressions?: {
|
|
2234
2717
|
daily_average?: number;
|
|
2235
2718
|
total?: number;
|
|
2236
2719
|
/** Percentage change compared to previous period */
|
|
2237
2720
|
trend_percent?: number;
|
|
2238
2721
|
};
|
|
2722
|
+
/** `days` is 7, 30 or 90; any other `?period=` value silently becomes 7. `start` is midnight (API-server local time) that many days back and `end` is the moment the request was served, so the window is not a whole number of days. */
|
|
2239
2723
|
period?: {
|
|
2240
2724
|
days?: number;
|
|
2241
2725
|
end?: string;
|
|
2242
2726
|
start?: string;
|
|
2243
2727
|
};
|
|
2728
|
+
/** `location` is a `City, State` string or `null` when either part is missing. `online_status` comes from the live heartbeat mirror and is `online`/`offline`/`unknown`, where `unknown` means the screen has never heartbeated — it is not a synonym for offline. `status` is the separate approval state. */
|
|
2244
2729
|
screen?: {
|
|
2245
2730
|
id?: string;
|
|
2246
2731
|
location?: string;
|
|
@@ -2248,6 +2733,7 @@ type ScreenAnalyticsResponse = {
|
|
|
2248
2733
|
online_status?: string;
|
|
2249
2734
|
status?: string;
|
|
2250
2735
|
};
|
|
2736
|
+
/** One entry per day that had impressions — days with none are absent, not zero-filled, which is why `daily_average` and this array can disagree with the period length. **`avg_duration_ms` is always `0`**: it is a hardcoded literal in both the ClickHouse query and the Postgres fallback. */
|
|
2251
2737
|
timeline?: Array<{
|
|
2252
2738
|
avg_duration_ms?: number;
|
|
2253
2739
|
date?: string;
|
|
@@ -2263,13 +2749,17 @@ type ScreenAnalyticsResponse = {
|
|
|
2263
2749
|
*/
|
|
2264
2750
|
type ScreenGroup = {
|
|
2265
2751
|
created_at?: string;
|
|
2752
|
+
/** Free-text note. Returned as an explicit `null` rather than omitted when unset, and an empty string supplied at create is stored as `null`. A PATCH writes only the keys you actually send, so omitting it preserves the current value. */
|
|
2266
2753
|
description?: string | null;
|
|
2267
|
-
/** Screen group id (sg_
|
|
2754
|
+
/** Screen group id: the literal prefix `sg_` followed by a v4 UUID (`pgRepo/screenGroups.js` mints `sg_${crypto.randomUUID()}`). Treat it as an opaque string; do not parse the UUID half. */
|
|
2268
2755
|
id?: string;
|
|
2756
|
+
/** Required, non-empty label. It is NOT unique and has no length constraint in the database, so two of your groups may share a name; `id` (`sg_<uuid>`) is the only stable handle. */
|
|
2269
2757
|
name?: string;
|
|
2758
|
+
/** Constant string `screen_group` — the Stripe-style type discriminator. It is present on every group payload, including inside a webhook's `data`, so a single handler can switch on it. */
|
|
2270
2759
|
object?: string;
|
|
2271
2760
|
/** Distinct member screens (COUNT and member list share one junction — they cannot diverge). */
|
|
2272
2761
|
screen_count?: number;
|
|
2762
|
+
/** Bumped by membership changes as well as metadata edits — `addMembers`, `removeMembers` and the soft delete all set `updated_at = NOW()` on the group row — so it is a change marker for the group INCLUDING its member set, not just its name and description. */
|
|
2273
2763
|
updated_at?: string;
|
|
2274
2764
|
};
|
|
2275
2765
|
/**
|
|
@@ -2282,27 +2772,49 @@ type ScreenGroup = {
|
|
|
2282
2772
|
*
|
|
2283
2773
|
*/
|
|
2284
2774
|
type ScreenGroupContentPolicy = {
|
|
2775
|
+
/** Rejects a direct ad only when `false` AND the ad's classification carries `contains_adult_content`. A `false` also adds `IAB25` plus `IAB25-2`..`IAB25-5` to the `bcat` and `sexually_suggestive`/`sexual_health`/`sensationalism` to the GAM `excl_cat`; unset falls back to the platform default of `false`. */
|
|
2285
2776
|
allow_adult_content?: boolean;
|
|
2777
|
+
/** Rejects a direct ad only when set to `false` AND the ad's classification carries `contains_alcohol`. A `false` also adds `IAB8-5`/`IAB8-18` to the outgoing OpenRTB `bcat` and `alcohol` to the GAM `excl_cat`; when the key is unset that list-building step uses the platform default, which for alcohol is `true` (not blocked). */
|
|
2286
2778
|
allow_alcohol?: boolean;
|
|
2779
|
+
/** Rejects a direct ad only when `false` AND the ad's classification carries `contains_gambling`. A `false` also adds `IAB9-7` to the `bcat` and `gambling`/`online_gambling` to the GAM `excl_cat`; unset falls back to the platform default of `false`. */
|
|
2287
2780
|
allow_gambling?: boolean;
|
|
2781
|
+
/** Rejects a direct ad only when `false` AND the ad's classification carries `contains_political`. A `false` also adds `IAB11-4` to the `bcat` and `politics` to the GAM `excl_cat`; unset falls back to the platform default of `true` (allowed). */
|
|
2288
2782
|
allow_political?: boolean;
|
|
2783
|
+
/** Rejects a direct ad only when `false` AND the ad's classification carries `contains_profanity`. A `false` also adds `IAB25-2` to the `bcat` and `sensationalism` to the GAM `excl_cat`; unset falls back to the platform default of `false`. */
|
|
2289
2784
|
allow_profanity?: boolean;
|
|
2785
|
+
/** Rejects a direct ad only when `false` AND the ad's classification carries `contains_religious`. A `false` also adds `IAB23` to the `bcat` and `religion` to the GAM `excl_cat`; unset falls back to the platform default of `true` (allowed). */
|
|
2290
2786
|
allow_religious?: boolean;
|
|
2787
|
+
/** Rejects a direct ad only when `false` AND the ad's classification carries `contains_tobacco`. A `false` also adds `IAB25-6` to the `bcat` and `drugs_tobacco` to the GAM `excl_cat`; unset falls back to the platform default of `false`, so tobacco is blocked on the programmatic side even when you never configure it. */
|
|
2291
2788
|
allow_tobacco?: boolean;
|
|
2789
|
+
/** Rejects a direct ad only when `false` AND the ad's classification carries `contains_violence`. A `false` also adds `IAB25-3`/`IAB25-4` to the `bcat` and `violence`/`military_conflict` to the GAM `excl_cat`; unset falls back to the platform default of `false`. */
|
|
2292
2790
|
allow_violence?: boolean;
|
|
2791
|
+
/** Consulted only when non-empty, and matched against the ad's typed `iab_categories` with `categoriesMatch`, so IAB codes and human-readable names both work (categories are inferred from subjects/keywords when the ad carries none). Under `lenient` and `permissive` a non-match is allowed through anyway, which makes the list inert at those levels; it also adds +10 per match to the content match score. */
|
|
2293
2792
|
allowed_categories?: Array<string>;
|
|
2793
|
+
/** A SOFT preference, not a filter: it never rejects anything. Matching keywords add +3 each to the content match score used for prioritisation, and the approval path explicitly skips this list. */
|
|
2294
2794
|
allowed_keywords?: Array<string>;
|
|
2795
|
+
/** Matched by case-insensitive SUBSTRING containment against the ad's `moderation.analysis.subjects`, not equality. Consulted only when non-empty and inert under `lenient`/`permissive`, like `allowed_categories`; it also adds +5 per match to the content match score. */
|
|
2295
2796
|
allowed_subjects?: Array<string>;
|
|
2797
|
+
/** When `true` and the ad's classification says `is_known_brand`, approval returns IMMEDIATELY — the `blocked_categories`, `blocked_subjects`, `blocked_keywords` and every `allowed_*` check below it never run. The `allow_*` toggles and the age-rating check sit above it and still apply. */
|
|
2296
2798
|
auto_approve_known_brands?: boolean;
|
|
2799
|
+
/** Same matching as `allowed_categories` (IAB code or human-readable name, via `categoriesMatch`), but a single hit rejects the ad at every `preference_level`. The rejection reason reports the human-readable name rather than the code you stored. */
|
|
2297
2800
|
blocked_categories?: Array<string>;
|
|
2801
|
+
/** The auction `bcat` sent on OpenRTB bid requests for member screens. It is NOT part of `content_preferences`: on an individual screen the same list lives on `programmatic_settings.blocked_iab_categories`, and a non-empty one there is one of the three conditions that makes the screen layer explicit and bypasses this group policy entirely. */
|
|
2298
2802
|
blocked_iab_categories?: Array<string>;
|
|
2803
|
+
/** The keyword list that DOES block, unlike `allowed_keywords`. Case-insensitive substring containment against the ad's `moderation.analysis.keywords`, so a short entry matches inside longer words. */
|
|
2299
2804
|
blocked_keywords?: Array<string>;
|
|
2805
|
+
/** Case-insensitive substring containment against the ad's `moderation.analysis.subjects` — `bar` therefore also blocks `barbecue`, so use whole distinctive words. A hit is a hard reject. */
|
|
2300
2806
|
blocked_subjects?: Array<string>;
|
|
2807
|
+
/** When `false`, direct-ad moderation short-circuits and every ad is auto-approved for member screens. It does NOT disable brand safety on the programmatic side — the `allow_*` toggles are still translated into the OpenRTB `bcat` and GAM `excl_cat` regardless of this flag. */
|
|
2301
2808
|
content_filtering_enabled?: boolean;
|
|
2809
|
+
/** The group this row belongs to. It is also the tiebreak: a screen that is a member of more than one policy-bearing group inherits from the LOWEST `group_id`, so a screen should be in at most one. */
|
|
2302
2810
|
group_id?: string;
|
|
2811
|
+
/** Highest ad classification a member screen accepts. `G` is deliberately NOT the strictest setting — the matcher silently rewrites it to `PG-13` for backwards compatibility — and `preference_level` then adds its buffer on top, so `lenient` with `PG-13` actually admits `R`. */
|
|
2303
2812
|
max_age_rating?: "G" | "PG" | "PG-13" | "R" | "NC-17";
|
|
2813
|
+
/** Always the literal `screen_group_content_policy`. The policy is returned as `data.content_policy`, and a group carrying no policy of its own returns a bare `null` there — so on that path there is no `object` to branch on at all. */
|
|
2304
2814
|
object?: string;
|
|
2815
|
+
/** Selects a strictness profile, not a label. It sets the age-rating buffer (`strict`/`moderate` 0, `lenient` +1, `permissive` +2 notches above `max_age_rating`), whether an ad with no AI classification is auto-approved (`lenient`/`permissive` yes; `strict`/`moderate` reject and queue re-moderation), and how the `allowed_*` lists behave on a non-match — `strict` rejects, `moderate` lets common business categories through, `lenient`/`permissive` make them inert. */
|
|
2305
2816
|
preference_level?: "strict" | "moderate" | "lenient" | "permissive";
|
|
2817
|
+
/** ISO-8601 timestamp of the last write to this policy row, or `null` when the row has none. Member screens read the resolved policy through a screen-keyed Redis cache that a policy, membership or group-delete write busts, so an edit here reaches serving immediately rather than after the 60-second TTL. */
|
|
2306
2818
|
updated_at?: string;
|
|
2307
2819
|
} | null;
|
|
2308
2820
|
/**
|
|
@@ -2311,24 +2823,43 @@ type ScreenGroupContentPolicy = {
|
|
|
2311
2823
|
*
|
|
2312
2824
|
*/
|
|
2313
2825
|
type ScreenGroupContentPolicyInput = {
|
|
2826
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Writing `false` adds `IAB25` and `IAB25-2`..`IAB25-5` to the member screens' `bcat`; unset already blocks it via the platform default. */
|
|
2314
2827
|
allow_adult_content?: boolean;
|
|
2828
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Writing `false` both rejects classified alcohol creatives on member screens and adds `IAB8-5`/`IAB8-18` to their outgoing `bcat`. */
|
|
2315
2829
|
allow_alcohol?: boolean;
|
|
2830
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Writing `false` adds `IAB9-7` to the member screens' `bcat`; unset already blocks gambling programmatically via the platform default. */
|
|
2316
2831
|
allow_gambling?: boolean;
|
|
2832
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Writing `false` adds `IAB11-4` to the member screens' `bcat`; the platform default here is `true`, so political ads run unless you set this. */
|
|
2317
2833
|
allow_political?: boolean;
|
|
2834
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Writing `false` adds `IAB25-2` to the member screens' `bcat`; unset already blocks it via the platform default. */
|
|
2318
2835
|
allow_profanity?: boolean;
|
|
2836
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Writing `false` adds `IAB23` to the member screens' `bcat`; the platform default here is `true`, so religious ads run unless you set this. */
|
|
2319
2837
|
allow_religious?: boolean;
|
|
2838
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Writing `false` adds `IAB25-6` to the member screens' `bcat`; leaving it unset already blocks tobacco programmatically via the platform brand-safety default. */
|
|
2320
2839
|
allow_tobacco?: boolean;
|
|
2840
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Writing `false` adds `IAB25-3`/`IAB25-4` to the member screens' `bcat`; unset already blocks it via the platform default. */
|
|
2321
2841
|
allow_violence?: boolean;
|
|
2842
|
+
/** Array of strings, at most 100 items (`array_too_large`); a non-array is `field_must_be_array` and a non-string element is `array_items_must_be_strings`. Writing it REPLACES the stored list — there is no append — and the allow/block pair is conflict-checked case-insensitively against this patch MERGED onto the stored row, so it can 400 with `conflicting_lists` on a term you set in an earlier request. */
|
|
2322
2843
|
allowed_categories?: Array<string>;
|
|
2844
|
+
/** Array of strings, at most 100 items (`array_too_large`); a non-array is `field_must_be_array` and a non-string element is `array_items_must_be_strings`. Writing it REPLACES the stored list — there is no append — and it is conflict-checked against `blocked_keywords`, even though at serve time this list never rejects anything — it only contributes to the content match score. */
|
|
2323
2845
|
allowed_keywords?: Array<string>;
|
|
2846
|
+
/** Array of strings, at most 100 items (`array_too_large`); a non-array is `field_must_be_array` and a non-string element is `array_items_must_be_strings`. Writing it REPLACES the stored list — there is no append — and the Subjects allow/block pair is conflict-checked against the stored row merged with this patch. Note these are matched by substring at serve time, not equality. */
|
|
2324
2847
|
allowed_subjects?: Array<string>;
|
|
2848
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Setting it `true` makes a known-brand ad bypass every category, subject and keyword check on member screens, so treat it as a widening, not a convenience. */
|
|
2325
2849
|
auto_approve_known_brands?: boolean;
|
|
2850
|
+
/** Array of strings, at most 100 items (`array_too_large`); a non-array is `field_must_be_array` and a non-string element is `array_items_must_be_strings`. Writing it REPLACES the stored list — there is no append — and blocking a category an earlier PUT allowed fails with `conflicting_lists` even though this request names only one side of the pair. */
|
|
2326
2851
|
blocked_categories?: Array<string>;
|
|
2852
|
+
/** Array of strings capped at 100 by a DIFFERENT constant from the other lists (`PROGRAMMATIC_SETTINGS_LIMITS.max_blocked_iab_categories`), reported as `blocked_iab_categories_too_large`. It is not conflict-checked, because there is no `allowed_iab_categories` counterpart. */
|
|
2327
2853
|
blocked_iab_categories?: Array<string>;
|
|
2854
|
+
/** Array of strings, at most 100 items (`array_too_large`); a non-array is `field_must_be_array` and a non-string element is `array_items_must_be_strings`. Writing it REPLACES the stored list — there is no append — and unlike `allowed_keywords` this one really does reject, by case-insensitive substring match against the ad's analysed keywords. */
|
|
2328
2855
|
blocked_keywords?: Array<string>;
|
|
2856
|
+
/** Array of strings, at most 100 items (`array_too_large`); a non-array is `field_must_be_array` and a non-string element is `array_items_must_be_strings`. Writing it REPLACES the stored list — there is no append — and it is a hard reject at serve time, matched by case-insensitive substring — so a short entry blocks more than you intend. */
|
|
2329
2857
|
blocked_subjects?: Array<string>;
|
|
2858
|
+
/** Must be a real JSON boolean — the strings `"true"`/`"false"` are rejected with `field_must_be_boolean`, unlike the L-bar PUT which accepts them. Omitting the key leaves the stored value untouched; there is no way to reset a key back to "inherit". */
|
|
2330
2859
|
content_filtering_enabled?: boolean;
|
|
2860
|
+
/** One of `G`, `PG`, `PG-13`, `R`, `NC-17`; anything else is `invalid_age_rating`. Writing `G` does not give you the strictest policy — the matcher later treats `G` as `PG-13`. */
|
|
2331
2861
|
max_age_rating?: "G" | "PG" | "PG-13" | "R" | "NC-17";
|
|
2862
|
+
/** One of `strict`, `moderate`, `lenient`, `permissive`; anything else is rejected with `invalid_preference_level`. Validated against the same constant the per-screen content-preferences PUT uses, so the two surfaces cannot drift. */
|
|
2332
2863
|
preference_level?: "strict" | "moderate" | "lenient" | "permissive";
|
|
2333
2864
|
};
|
|
2334
2865
|
/**
|
|
@@ -2344,8 +2875,11 @@ type ScreenGroupContentPolicyInput = {
|
|
|
2344
2875
|
type ScreenGroupEvent = {
|
|
2345
2876
|
/** The screen_group / content-policy / sensing-config object. */
|
|
2346
2877
|
data?: Record<string, unknown>;
|
|
2878
|
+
/** The event type, also sent in the `X-Trillboards-Event` header and mirrored to the SSE stream at `GET /v1/partner/events/stream`. This whole event family is gated by the `SCREEN_GROUP_EVENTS_ENABLED` runtime flag, which is OFF by default — when off, nothing is enqueued at all. */
|
|
2347
2879
|
event?: "screen_group.created" | "screen_group.updated" | "screen_group.deleted" | "screen_group.member.added" | "screen_group.member.removed" | "screen_group.content_policy.updated" | "screen_group.sensing_config.updated";
|
|
2880
|
+
/** `evt_<base36 epoch ms>_<12 hex>`, minted once per occurrence. Use it as the idempotency key: a retried delivery of the same occurrence reuses it, but the same id is also sent to EVERY endpoint you have subscribed to that event, so dedupe per (endpoint, id). It is not the server-side delivery audit id and cannot be looked up. */
|
|
2348
2881
|
id?: string;
|
|
2882
|
+
/** Stamped when the server ENQUEUED the delivery, not when it was sent, so a retried delivery still carries the original value and can trail wall-clock by the retry backoff. It is not the resource's own `updated_at`. */
|
|
2349
2883
|
timestamp?: string;
|
|
2350
2884
|
};
|
|
2351
2885
|
/**
|
|
@@ -2363,14 +2897,23 @@ type ScreenGroupSensingConfig = {
|
|
|
2363
2897
|
periodicIntervalMs?: number | null;
|
|
2364
2898
|
vehicleTriggeredIntervalMs?: number | null;
|
|
2365
2899
|
} | null;
|
|
2900
|
+
/** Group override of the profile's declared frame-capture cadence in milliseconds; `null` inherits the base profile's value. Treat it as advisory - this legacy scalar does not reach the device capture loop (the edge `FrameCaptureManager` reads the nested `capture` block), and observed cadence was measured at ~12x the declared value against a fleet floor of one analysis per 30 s. */
|
|
2366
2901
|
capture_interval_ms?: number | null;
|
|
2902
|
+
/** When `false` the whole group step is a no-op and member screens fall straight through to the next layer of the resolver — the row is retained but contributes nothing, which is how you disable group sensing without losing the overrides. */
|
|
2367
2903
|
enabled?: boolean;
|
|
2904
|
+
/** Advisory workload cost of the profile, normally derived server-side as the sum of the selected models' registry `fps_cost` (built-ins run 5.5-12.0). It is display metadata only — no device or pipeline code reads it, so overriding it throttles nothing. */
|
|
2368
2905
|
estimated_fps_impact?: number | null;
|
|
2906
|
+
/** The `sg_`-prefixed Screen Group this config belongs to; echoes the `groupId` path parameter. */
|
|
2369
2907
|
group_id?: string;
|
|
2908
|
+
/** Detection-confidence floor in 0..1, overlaid onto the resolved profile's `thresholds.min_confidence`; built-in profiles ship 0.35-0.45. `null` inherits the base profile's value. */
|
|
2370
2909
|
min_confidence?: number | null;
|
|
2910
|
+
/** Stripe-style type discriminator; always the literal `screen_group_sensing_config`, so it is safe to switch on. */
|
|
2371
2911
|
object?: string;
|
|
2912
|
+
/** The sensing profile members inherit, bound by NAME (a builtin or an existing custom profile) rather than by `profile_id`, because `setDeployment` clones a fresh `profile_id` per screen on every redeploy. `null` means the group binds no profile. */
|
|
2372
2913
|
profile_name?: string | null;
|
|
2914
|
+
/** Group override of how often the device batches observations back to the server, in milliseconds; `null` inherits the base profile's value. Built-in profiles ship 15000-30000. */
|
|
2373
2915
|
report_interval_ms?: number | null;
|
|
2916
|
+
/** When this group's sensing config row was last written. The write also busts the resolver caches for the members as they stood at that instant and emits a `screen_group.sensing_config.updated` webhook, so a screen added to the group afterwards picks the config up on its own next cache miss instead. */
|
|
2374
2917
|
updated_at?: string;
|
|
2375
2918
|
/** Per-screen cloud-vision budget for member screens. screen_hourly_pace wins when both are set; otherwise derived from screen_daily_calls. null = no group budget (fleet keeps platform-default caps). */
|
|
2376
2919
|
vision_budget?: {
|
|
@@ -2392,11 +2935,17 @@ type ScreenGroupSensingConfigInput = {
|
|
|
2392
2935
|
periodicIntervalMs?: number | null;
|
|
2393
2936
|
vehicleTriggeredIntervalMs?: number | null;
|
|
2394
2937
|
} | null;
|
|
2938
|
+
/** Frame-capture cadence override in milliseconds (100..3600000); `null` means inherit from the base profile. Advisory in practice: this legacy scalar does not reach the device capture loop (the edge reads the nested `capture` block), the fleet floors at one analysis per 30 s, and observed cadence was measured at ~12x the declared value. */
|
|
2395
2939
|
capture_interval_ms?: number | null;
|
|
2940
|
+
/** Set `false` to make the group step a no-op — members fall through to the next resolver layer while the overrides stay stored, so this is the reversible way to switch group sensing off. */
|
|
2396
2941
|
enabled?: boolean;
|
|
2942
|
+
/** Advisory workload-cost number (must be >= 0); `null` inherits the profile's own value, which the server derives from the selected models' registry `fps_cost`. Purely descriptive — setting it changes what readback and operator surfaces display and throttles nothing. */
|
|
2397
2943
|
estimated_fps_impact?: number | null;
|
|
2944
|
+
/** Detection-confidence floor in 0..1 written into the resolved profile's `thresholds.min_confidence`; `null` inherits the base profile's value (built-ins are 0.35-0.45). */
|
|
2398
2945
|
min_confidence?: number | null;
|
|
2946
|
+
/** Bind the group to a builtin or existing custom profile BY NAME; a name that resolves to neither is a 400. `null` clears the binding. Never a `profile_id` — that is re-minted per screen on each redeploy. */
|
|
2399
2947
|
profile_name?: string | null;
|
|
2948
|
+
/** Observation-reporting cadence override in milliseconds (100..3600000); `null` means inherit from the base profile. */
|
|
2400
2949
|
report_interval_ms?: number | null;
|
|
2401
2950
|
/** Per-screen cloud-vision budget block; null clears it. Unknown keys are rejected (400). */
|
|
2402
2951
|
vision_budget?: {
|
|
@@ -2412,30 +2961,47 @@ type SensingPerformanceMode = "auto" | "presence_only" | "demographics" | "advan
|
|
|
2412
2961
|
* A Sense Anything Route — a (condition → channel actions) alert/automation rule.
|
|
2413
2962
|
*/
|
|
2414
2963
|
type SensingRoute = {
|
|
2964
|
+
/** Every channel action fired when the route matches; 1 to 10 per route, each with its own `enabled` flag so one can be muted without deleting the route. `webhook` is the only channel with no external side effect — `whatsapp`, `sms`, `email` and `slack` deliver a real message to a real person, so point a test route at `webhook`. */
|
|
2415
2965
|
actions?: Array<SensingRouteAction>;
|
|
2416
2966
|
/** One bit per weekday (127 = always). */
|
|
2417
2967
|
activeDaysMask?: number;
|
|
2968
|
+
/** Inclusive end of the daily active window, in minutes past local midnight in `activeTz` (0..1439). When it is LESS than `activeStartMinute` the window is read as overnight — 1200 to 360 means 20:00 through 06:00 — rather than as an empty range. */
|
|
2418
2969
|
activeEndMinute?: number | null;
|
|
2970
|
+
/** Inclusive start of the daily active window, in minutes past local midnight in `activeTz` (0..1439; 1200 = 20:00). Ignored unless `activeTz` is set, and a `null` on either end makes the window whole-day. */
|
|
2419
2971
|
activeStartMinute?: number | null;
|
|
2972
|
+
/** IANA zone name (e.g. `America/New_York`) the active window is evaluated in, trimmed to 64 characters. `null` disables the ENTIRE active-window check — `activeStartMinute`, `activeEndMinute` and `activeDaysMask` do nothing without it — and a zone the runtime cannot parse fails OPEN (always active) rather than silently dropping alerts. */
|
|
2420
2973
|
activeTz?: string | null;
|
|
2974
|
+
/** `null` on a `count`-metric digest route (there is nothing to aggregate over). On an `avg`/`max` digest it carries only `field`, with `op` and `value` absent — only a `threshold`-mode route has a complete condition. */
|
|
2421
2975
|
condition?: SensingRouteCondition | null;
|
|
2422
2976
|
createdAt?: string;
|
|
2977
|
+
/** Minimum gap in seconds between alerts for the same (route, screen) episode; clamped to 0..86400 and defaulting to 300. It also caps the falling-edge clear window (60 s maximum), so a shorter debounce re-arms the route sooner as well as spacing alerts more tightly. */
|
|
2423
2978
|
debounceSeconds?: number;
|
|
2979
|
+
/** Scheduled-aggregate configuration; `null` on `threshold` routes. `nextRunAt` is a server-held cursor the digest cron advances after every send, so it moves on its own without any edit from you. */
|
|
2424
2980
|
digest?: {
|
|
2425
2981
|
intervalMinutes?: number;
|
|
2426
2982
|
metric?: "count" | "avg" | "max";
|
|
2427
2983
|
nextRunAt?: string;
|
|
2428
2984
|
windowMinutes?: number;
|
|
2429
2985
|
} | null;
|
|
2986
|
+
/** Whether the evaluator considers this route at all. A disabled route keeps its condition, actions and screen set and is simply skipped by every hot-path read; deleting a route also sets this `false` (rows are soft-deleted, never removed). */
|
|
2430
2987
|
enabled?: boolean;
|
|
2988
|
+
/** `threshold` evaluates `condition` against each incoming observation. `digest` ignores the condition and instead sends a scheduled aggregate driven by the `digest.nextRunAt` cursor; focus regions are rejected on digest routes. */
|
|
2431
2989
|
mode?: "threshold" | "digest";
|
|
2990
|
+
/** Operator-supplied label, trimmed and capped at 200 characters. It is delivered verbatim as `route_name` on every fire but is not the alert text — the human-readable message is composed separately from the screen's sensing intent. */
|
|
2432
2991
|
name?: string;
|
|
2992
|
+
/** The owning principal's id, compared against your credential on every read and write — a mismatch answers 404 (existence-hidden), never 403. Equal to your partner id for routes created here. */
|
|
2433
2993
|
ownerId?: string;
|
|
2994
|
+
/** Which principal created the route: `partner` for anything created through this API, `earner_user` for a route the screen's own operator created in the earner portal. The Partner API only ever lists or edits `partner` routes, but an `earner_user` route on your screen still delivers webhooks to you. */
|
|
2434
2995
|
ownerKind?: string;
|
|
2996
|
+
/** The partner the route was created under. Equal to `ownerId` for partner-created routes and `null` for a route an earner created in the portal, so it is not a reliable join key for the screen's partner. */
|
|
2435
2997
|
partnerId?: string;
|
|
2998
|
+
/** Binds the route to a sensing profile BY NAME, never by `profile_id` — redeploying a profile mints a fresh `profile_id` per screen, which would orphan the binding. `null` means unbound; when set, the route stays dormant on any screen whose active profile has a different name. */
|
|
2436
2999
|
profileName?: string | null;
|
|
3000
|
+
/** Server-minted immutable id: the literal prefix `rt_` followed by a UUID. Webhook consumers should key on this rather than `route_name`, which the owner can rename at any time. */
|
|
2437
3001
|
routeId?: string;
|
|
3002
|
+
/** Per-camera areas, at most one entry per screen, and NOT geometrically enforced. The server turns each into a single line of prompt text capped at 120 characters; measured 2026-08-25 on `gemini-3.1-flash-lite`, both the rectangle and the lasso form leaked 100% at every anchor, so a focused alert still reacts to the whole frame. */
|
|
2438
3003
|
screenFocusRegions?: Array<SensingRouteScreenFocusRegion>;
|
|
3004
|
+
/** The screens this route watches — deduped and capped at 250. For a group-targeted route these are the group's LIVE members resolved at read time, not a stored set, so the list can change between two GETs without any edit. */
|
|
2439
3005
|
screenMongoIds?: Array<string>;
|
|
2440
3006
|
/** DEPRECATED and rejected on write with 400. A duration expressed as N readings inside a fixed window can only be satisfied by a screen reporting faster than window/(N-1) seconds, and nothing knows that rate when the route is saved - so the route saves cleanly and then never fires at all. Send sustainSeconds instead. Still honoured for rows that already carry it. */
|
|
2441
3007
|
sustainMinSamples?: number;
|
|
@@ -2445,6 +3011,7 @@ type SensingRoute = {
|
|
|
2445
3011
|
sustainWindowSeconds?: number;
|
|
2446
3012
|
/** The sg_ Screen Group this route targets. When set, the route fires on the group's LIVE members (screenMongoIds reflects the resolved members at read time) and no explicit screens are stored. */
|
|
2447
3013
|
targetGroupId?: string | null;
|
|
3014
|
+
/** Last write to the route row — NOT a configuration-change stamp. The digest cron bumps it every time it advances a digest route's next-run cursor, and a soft delete bumps it too, so it moves on rows nobody edited. */
|
|
2448
3015
|
updatedAt?: string;
|
|
2449
3016
|
};
|
|
2450
3017
|
/**
|
|
@@ -2453,6 +3020,7 @@ type SensingRoute = {
|
|
|
2453
3020
|
type SensingRouteAction = {
|
|
2454
3021
|
/** Attach the evidence frame/clip (people-facing channels). */
|
|
2455
3022
|
attachClip?: boolean;
|
|
3023
|
+
/** Selects a strict per-channel shape: `webhook` requires `webhookId`, `whatsapp`/`sms` require `recipientE164`, `email` requires `recipientEmail`, `slack` requires `slackChannelId`, and `clip` needs no destination. Any other destination field on the action is rejected, and `webhook` is the only channel that does not deliver to a person — use it for test routes. */
|
|
2456
3024
|
channel: "webhook" | "whatsapp" | "sms" | "slack" | "email" | "clip";
|
|
2457
3025
|
/** E.164 phone number (channel=whatsapp|sms). */
|
|
2458
3026
|
recipientE164?: string;
|
|
@@ -2471,6 +3039,7 @@ type SensingRouteAction = {
|
|
|
2471
3039
|
type SensingRouteCondition = {
|
|
2472
3040
|
/** The sensed field key (snake_case) to evaluate. */
|
|
2473
3041
|
field: string;
|
|
3042
|
+
/** How the observed value is compared with `value`. `gt`/`gte`/`lt`/`lte` match ONLY when both sides parse as finite numbers; `eq`/`neq` fall back to strict equality for booleans and non-numeric strings. A missing or null observation never matches — including `neq`, which does not fire on absence. */
|
|
2474
3043
|
op: "gt" | "gte" | "lt" | "lte" | "eq" | "neq";
|
|
2475
3044
|
/** The threshold value (numeric, string, or boolean depending on the field). */
|
|
2476
3045
|
value?: number | string | boolean;
|
|
@@ -2479,18 +3048,24 @@ type SensingRouteCondition = {
|
|
|
2479
3048
|
* A normalized camera area used only by this alert. The rectangle is the compatibility bounding box; optional ordered points define the exact lasso perimeter. Omitted cameras use the whole view.
|
|
2480
3049
|
*/
|
|
2481
3050
|
type SensingRouteScreenFocusRegion = {
|
|
3051
|
+
/** Rectangle height as a fraction of frame height, at least 0.05. When `points` is supplied this is DERIVED from the lasso's bounding box. Note the region is only ever sent to the model as prompt text and is not geometrically enforced — measured 2026-08-25, both shapes leaked 100% at every anchor, so a focused alert reacts to the whole frame. */
|
|
2482
3052
|
height: number;
|
|
2483
3053
|
/** Ordered non-self-intersecting lasso vertices, normalized to a 0.01 coordinate grid so the saved perimeter is represented exactly in model instructions. The perimeter closes from the last point to the first. */
|
|
2484
3054
|
points?: Array<{
|
|
2485
3055
|
x: number;
|
|
2486
3056
|
y: number;
|
|
2487
3057
|
}>;
|
|
3058
|
+
/** Which of the route's cameras this area applies to. It must appear in the route's explicit `screenMongoIds` and may appear at most once across the array, or the write is a 400; cameras with no entry are watched whole-view. */
|
|
2488
3059
|
screenMongoId: string;
|
|
3060
|
+
/** Rectangle width as a fraction of frame width, at least 0.05. When `points` is supplied this is DERIVED from the lasso's bounding box rather than read from the request, so older readers keep a safe approximation of the polygon. */
|
|
2489
3061
|
width: number;
|
|
3062
|
+
/** Left edge of the bounding rectangle as a fraction of frame width, origin top-left. Stored rounded to 4 decimal places, and capped at 0.95 because `width` has a 0.05 minimum and `x + width` may not exceed 1. */
|
|
2490
3063
|
x: number;
|
|
3064
|
+
/** Top edge of the bounding rectangle as a fraction of frame height, origin top-left. Stored rounded to 4 decimal places, and capped at 0.95 because `height` has a 0.05 minimum and `y + height` may not exceed 1. */
|
|
2491
3065
|
y: number;
|
|
2492
3066
|
};
|
|
2493
3067
|
type SensingSettingsResponse = {
|
|
3068
|
+
/** Summary of the profile row currently deployed and active on this screen (id, name, cadences and derived capabilities). `null` when the screen has never had a profile deployed, in which case the response's mode fields describe the default rather than anything running. */
|
|
2494
3069
|
active_profile?: {
|
|
2495
3070
|
capabilities?: Array<string>;
|
|
2496
3071
|
capture_interval_ms?: number | null;
|
|
@@ -2511,11 +3086,14 @@ type SensingSettingsResponse = {
|
|
|
2511
3086
|
telemetry_fresh?: boolean;
|
|
2512
3087
|
};
|
|
2513
3088
|
effective_mode?: SensingPerformanceMode;
|
|
3089
|
+
/** Despite the name this is not an apply OUTCOME and carries no device acknowledgement: it is `null` when the screen has no active profile and otherwise always `{ status: "active_profile_loaded", profile_id, updated_at }` describing the profile row that is loaded now. */
|
|
2514
3090
|
last_apply_status?: Record<string, unknown> | null;
|
|
3091
|
+
/** The full selectable set with human labels (`auto` = "Auto recommended", `presence_only` = "Viewer count only", and so on). It is a static list, NOT filtered by this screen's hardware — a mode listed here can still be refused; read `safety.blocks` before offering it. */
|
|
2515
3092
|
modes?: Array<{
|
|
2516
3093
|
label?: string;
|
|
2517
3094
|
value?: SensingPerformanceMode;
|
|
2518
3095
|
}>;
|
|
3096
|
+
/** Hardware pre-flight for `effective_mode`. `allowed` is simply `blocks.length === 0`; `warnings` never block (stale telemetry, recent device failures), while a `block` — missing fresh telemetry for a load-increasing mode, or FLOOR-tier/CPU-only hardware for `advanced` — makes a PUT of that mode answer 409 with this object under `error.details.safety`. */
|
|
2519
3097
|
safety?: {
|
|
2520
3098
|
allowed?: boolean;
|
|
2521
3099
|
blocks?: Array<{
|
|
@@ -2527,6 +3105,7 @@ type SensingSettingsResponse = {
|
|
|
2527
3105
|
message?: string;
|
|
2528
3106
|
}>;
|
|
2529
3107
|
};
|
|
3108
|
+
/** The screen's Mongo id — the same identifier the route surfaces call `screenMongoId`. Echoes the path parameter. */
|
|
2530
3109
|
screen_id?: string;
|
|
2531
3110
|
};
|
|
2532
3111
|
/**
|
|
@@ -2543,13 +3122,17 @@ type SensingSettingsResponse = {
|
|
|
2543
3122
|
type SensingThresholdCrossedEvent = {
|
|
2544
3123
|
/** The sensed field key evaluated (null for count-only digests). */
|
|
2545
3124
|
field?: string | null;
|
|
3125
|
+
/** Server clock when the evaluator fired — not when the camera saw it. On the cloud path it was measured 8.2 s median and 15.3 s p95 behind the frame's shutter, so use the payload's `captured_at` (the device's own shutter instant) whenever the fire carried a frame. */
|
|
2546
3126
|
fired_at?: string;
|
|
2547
3127
|
/** The observed value that crossed the threshold. */
|
|
2548
3128
|
observed?: unknown;
|
|
3129
|
+
/** The comparison the evaluator applied. `null` for a scheduled digest, which aggregates rather than comparing. Numeric operators only ever match when both the observed value and the threshold parse as finite numbers. */
|
|
2549
3130
|
op?: "gt" | "gte" | "lt" | "lte" | "eq" | "neq" | null;
|
|
3131
|
+
/** Who owns the route that fired. Webhooks are dispatched to the SCREEN's partner regardless of who created the route, so a partner endpoint legitimately receives `earner_user` fires for routes the screen's own operator configured and which are not visible through the Partner API. */
|
|
2550
3132
|
owner_kind?: "earner_user" | "partner";
|
|
2551
3133
|
/** The Route that fired. */
|
|
2552
3134
|
route_id?: string;
|
|
3135
|
+
/** The route's operator-supplied label at the time of the fire. It is mutable and not unique — match on `route_id` for idempotency and routing. */
|
|
2553
3136
|
route_name?: string;
|
|
2554
3137
|
/** The screen (mongo id) whose observation crossed the threshold. */
|
|
2555
3138
|
screen_id?: string;
|
|
@@ -2557,46 +3140,70 @@ type SensingThresholdCrossedEvent = {
|
|
|
2557
3140
|
threshold?: unknown;
|
|
2558
3141
|
};
|
|
2559
3142
|
type SsdpDeviceInfo = {
|
|
3143
|
+
/** The `<friendlyName>` element from the fetched description XML. If it looks like a personal name (possessive apostrophe-s, an email, a MAC) the ENTIRE row is discarded rather than the field being blanked, so consumer devices named after their owner never appear at all. */
|
|
2560
3144
|
friendlyName?: string | null;
|
|
3145
|
+
/** The `LOCATION:` header from the M-SEARCH response — the URL of the device description XML, and the de-duplication key for the whole list (one row per distinct LOCATION, last response wins). A response without it produces no row at all. */
|
|
2561
3146
|
location: string;
|
|
3147
|
+
/** The `<manufacturer>` element from the description XML. `null` whenever the description fetch was skipped, timed out or failed — which is common, since fetching is bounded to half the listen window with a 1.5 s per-host timeout — so absence says nothing about the device. */
|
|
2562
3148
|
manufacturer?: string | null;
|
|
3149
|
+
/** The `<modelName>` element from the description XML, field-stripped (not row-dropped) if it contains an email or a MAC. Absent on every row whose description fetch did not complete. */
|
|
2563
3150
|
modelName?: string | null;
|
|
3151
|
+
/** The raw `SERVER:` response header, an RFC 2616 product token such as `Linux UPnP/1.0 Sonos/72.6-31040`. It comes from the M-SEARCH response itself, so unlike the XML-derived fields below it is present even when the description fetch fails — and it is deliberately neither PII-filtered nor hashed server-side, being treated as a low-cardinality vendor hint. */
|
|
2564
3152
|
server?: string | null;
|
|
3153
|
+
/** The `ST:` search-target header of the response, e.g. `urn:schemas-upnp-org:device:ZonePlayer:1`. The probe searches with `ST: ssdp:all`, so this reflects what the device chose to answer as, not what we asked for. It arrives with the M-SEARCH response, so it survives a failed description fetch; an empty string means the responder omitted the header. */
|
|
2565
3154
|
st: string;
|
|
3155
|
+
/** The `<UDN>` element — the device's self-declared Unique Device Name, typically a `uuid:` URN and the most stable per-device key SSDP offers. It is hashed server-side, and it is absent whenever the description fetch did not complete. */
|
|
2566
3156
|
udn?: string | null;
|
|
2567
3157
|
[key: string]: unknown;
|
|
2568
3158
|
};
|
|
2569
3159
|
type TeamInviteResponse = {
|
|
3160
|
+
/** The invited address, lower-cased and trimmed. Once the invitation is accepted the listing reports the accepting user account's email instead, which may differ from this one. */
|
|
2570
3161
|
email?: string;
|
|
2571
3162
|
/** When the invitation expires (7 days from creation) */
|
|
2572
3163
|
expires_at?: string;
|
|
3164
|
+
/** A freshly minted 24-character hex ObjectId held as the membership's legacy id — NOT the `memberships` UUID primary key. It is the value the team routes take as the member path parameter. */
|
|
2573
3165
|
membership_id?: string;
|
|
3166
|
+
/** Only `admin` and `viewer` can be invited — `owner` is the membership created with the partner account and cannot be granted through this endpoint. */
|
|
2574
3167
|
role?: "admin" | "viewer";
|
|
3168
|
+
/** `active` when the invited email already had a Trillboards account: the membership is auto-accepted at invite time, `accepted_at` is set immediately, no token is issued and `expires_at` is `null`. `pending` otherwise, with a 7-day invite token. Either way the team is capped at 50 active-or-pending members. */
|
|
2575
3169
|
status?: "pending" | "active";
|
|
2576
3170
|
};
|
|
2577
3171
|
type TeamMember = {
|
|
3172
|
+
/** `null` while `status` is `pending`. Set at INVITE time rather than click time when the invitee already had a Trillboards account and the membership was auto-activated. */
|
|
2578
3173
|
accepted_at?: string | null;
|
|
3174
|
+
/** The linked user account's email once the invitation is accepted; the address the invitation was SENT to while `status` is `pending`. The two differ if the invitee accepts with a different account. */
|
|
2579
3175
|
email?: string;
|
|
3176
|
+
/** When the invitation row was created. Re-inviting the same address rotates the token and moves this timestamp forward. */
|
|
2580
3177
|
invited_at?: string;
|
|
3178
|
+
/** Name and email of the inviting user, resolved by join. `null` for the owner membership created with the partner account, and for any invitation whose inviter row no longer resolves. */
|
|
2581
3179
|
invited_by?: {
|
|
2582
3180
|
email?: string;
|
|
2583
3181
|
name?: string;
|
|
2584
3182
|
} | null;
|
|
3183
|
+
/** **Always `null`.** The column has no counterpart in `memberships` after the tenant-membership cutover, so the canonical reader projects a literal `NULL::timestamptz` and nothing writes it. */
|
|
2585
3184
|
last_portal_access?: string | null;
|
|
2586
3185
|
/** Unique membership record ID */
|
|
2587
3186
|
membership_id?: string;
|
|
3187
|
+
/** The linked user account's display name. Undefined — and so omitted from the JSON — while `status` is `pending`, because no user row is attached until the invitation is accepted. */
|
|
2588
3188
|
name?: string;
|
|
2589
3189
|
permissions?: RolePermissions;
|
|
3190
|
+
/** Profile photo URL from the linked user account, and null/absent while the invitation is pending for the same reason as `name`. */
|
|
2590
3191
|
photo?: string | null;
|
|
3192
|
+
/** `owner` is the membership created with the partner account and can never be invited or granted — only `admin` and `viewer` are grantable. Role alone determines `permissions`. */
|
|
2591
3193
|
role?: "owner" | "admin" | "viewer";
|
|
3194
|
+
/** The listing returns `active` + `pending` only; `revoked` rows appear only when `?status=revoked` is passed explicitly. An invitation to an email that already has a Trillboards account is auto-accepted and lands `active` immediately. */
|
|
2592
3195
|
status?: "active" | "pending" | "revoked";
|
|
2593
3196
|
/** User ID (null if pending invitation) */
|
|
2594
3197
|
user_id?: string;
|
|
2595
3198
|
};
|
|
2596
3199
|
type Transaction = {
|
|
3200
|
+
/** Integer US cents. Earnings rows are allocated cumulatively so the list sums EXACTLY to the lifetime total — an individual row can therefore differ by one cent from rounding that day's dollars on its own. */
|
|
2597
3201
|
amount_cents?: number;
|
|
3202
|
+
/** For a `payout`, the real creation timestamp. For an `earning` it is `new Date(period_date)` — midnight UTC of the accounting day, a value derived from the date key rather than the time anything happened. */
|
|
2598
3203
|
created_at?: string;
|
|
3204
|
+
/** For an `earning` row, the `YYYY-MM-DD` accounting period the aggregate covers. For a `payout` row it is the payout record's creation timestamp, so it carries a time component despite the declared `date` format. */
|
|
2599
3205
|
date?: string;
|
|
3206
|
+
/** Synthetic and namespaced by origin, never a ledger primary key: earnings rows are `daily_<YYYY-MM-DD>` (one per accounting day, not per impression); payouts are `admin_batch:<ledger_id>`, `earner_withdrawal:<withdrawal_id>` or `legacy_transfer:<id>`. */
|
|
2600
3207
|
id?: string;
|
|
2601
3208
|
/** Number of impressions (for earning transactions) */
|
|
2602
3209
|
impressions?: number;
|
|
@@ -2604,6 +3211,7 @@ type Transaction = {
|
|
|
2604
3211
|
source?: string;
|
|
2605
3212
|
/** Stripe transfer ID (for payout transactions) */
|
|
2606
3213
|
stripe_transfer_id?: string;
|
|
3214
|
+
/** `earning` rows are daily revenue aggregates from `programmatic_earnings_daily`; `payout` rows are money leaving, unioned from three sources and deduplicated by provider transfer id. `?type=all` (the default) merges both and sorts newest-first. */
|
|
2607
3215
|
type?: "earning" | "payout";
|
|
2608
3216
|
};
|
|
2609
3217
|
/**
|
|
@@ -2641,24 +3249,35 @@ type TrillboardsError = {
|
|
|
2641
3249
|
* Partial, non-clobbering update — include only the fields to change.
|
|
2642
3250
|
*/
|
|
2643
3251
|
type UpdatePartnerSensingRouteRequest = {
|
|
3252
|
+
/** REPLACES the entire action list (1..10 entries); the existing rows are deleted and reinserted. Omit the key to leave actions untouched — there is no way to add or remove a single action. */
|
|
2644
3253
|
actions?: Array<SensingRouteAction>;
|
|
3254
|
+
/** Seven-bit weekday mask evaluated in `activeTz`: bit 0 is Monday through bit 6 is Sunday, so 127 is every day and 0 never fires. Requires `activeTz`, like the rest of the active window. */
|
|
2645
3255
|
activeDaysMask?: number;
|
|
3256
|
+
/** Inclusive end of the daily active window, minutes past local midnight in `activeTz` (0..1439). A value below `activeStartMinute` is read as an overnight window, not an empty one. */
|
|
2646
3257
|
activeEndMinute?: number;
|
|
3258
|
+
/** Inclusive start of the daily active window, minutes past local midnight in `activeTz` (0..1439). Requires `activeTz` to have any effect. */
|
|
2647
3259
|
activeStartMinute?: number;
|
|
3260
|
+
/** IANA zone name the active window is evaluated in. Setting the minute/day fields without this leaves the window inert, and an unparseable zone fails open (always active) rather than suppressing alerts. */
|
|
2648
3261
|
activeTz?: string;
|
|
2649
3262
|
condition?: SensingRouteCondition;
|
|
3263
|
+
/** Minimum gap in seconds between alerts for the same (route, screen) episode, clamped to 0..86400. `0` means fire on every rising edge with no spacing at all. */
|
|
2650
3264
|
debounceSeconds?: number;
|
|
3265
|
+
/** Replaces the digest block and, as a side effect, rewrites `condition` to `{ field }` or `null`. `field` is required for `avg` and `max`; `intervalMinutes` must be at least 5 because the digest cron only runs every 5 minutes. */
|
|
2651
3266
|
digest?: {
|
|
2652
3267
|
field?: string;
|
|
2653
3268
|
intervalMinutes?: number;
|
|
2654
3269
|
metric?: "count" | "avg" | "max";
|
|
2655
3270
|
windowMinutes?: number;
|
|
2656
3271
|
};
|
|
3272
|
+
/** Only the JSON literal `true` enables the route here — every other value, including the string `"true"` and `1`, is written as `false`. This is the mirror image of create, where only the literal `false` disables, so send a real boolean. */
|
|
2657
3273
|
enabled?: boolean;
|
|
3274
|
+
/** Replaces the label. Must be a non-empty string after trimming and at most 200 characters; sending an empty or whitespace-only name is a 400 rather than a clear. */
|
|
2658
3275
|
name?: string;
|
|
3276
|
+
/** Rebinds the route to a different named sensing profile; send `null` (or an empty/whitespace string) to unbind it. Always a profile NAME — a `profile_id` is re-minted on every redeploy and would orphan the binding. */
|
|
2659
3277
|
profileName?: string | null;
|
|
2660
3278
|
/** Replace all selected rectangles or polygon lassos for the route; send [] to return every camera to whole view. Entries must target the route's explicit screenMongoIds. */
|
|
2661
3279
|
screenFocusRegions?: Array<SensingRouteScreenFocusRegion>;
|
|
3280
|
+
/** REPLACES the watched screen set (1..250, deduped); mutually exclusive with `targetGroupId` in one request. Focus regions for cameras that survive the swap are PRESERVED unless you also send `screenFocusRegions` (including `[]` to clear them). */
|
|
2662
3281
|
screenMongoIds?: Array<string>;
|
|
2663
3282
|
/** DEPRECATED and rejected on write with 400. A duration expressed as N readings inside a fixed window can only be satisfied by a screen reporting faster than window/(N-1) seconds, and nothing knows that rate when the route is saved - so the route saves cleanly and then never fires at all. Send sustainSeconds instead. Still honoured for rows that already carry it. */
|
|
2664
3283
|
sustainMinSamples?: number;
|
|
@@ -2674,6 +3293,7 @@ type UpdatePartnerSensingRouteRequest = {
|
|
|
2674
3293
|
*
|
|
2675
3294
|
*/
|
|
2676
3295
|
type VastWaterfall = {
|
|
3296
|
+
/** Derived, not configured: it is simply `sources.length > 0`. `false` means no demand source resolved for this screen on this request, so fall back to your own content. It is not a partner-settable toggle and it does not mean programmatic serving is switched off for the screen. */
|
|
2677
3297
|
enabled?: boolean;
|
|
2678
3298
|
/** Every source must be attempted, by `priority` ascending, until one actually plays. Not a preference list. */
|
|
2679
3299
|
sources?: Array<VastWaterfallSource>;
|
|
@@ -2684,7 +3304,9 @@ type VastWaterfall = {
|
|
|
2684
3304
|
* One demand source in the VAST waterfall. Fetch `vast_url` (already a Trillboards vast-wrap URL — use as-is) and pass the response to your IMA SDK. A failure here means "advance to the next source", not "the request had no demand".
|
|
2685
3305
|
*/
|
|
2686
3306
|
type VastWaterfallSource = {
|
|
3307
|
+
/** Identical across every source in this response — one id per waterfall build, so it groups all of a single ad opportunity's source attempts, while `request_id` identifies this source alone. Echo it back; never mint your own. */
|
|
2687
3308
|
auction_request_id?: string;
|
|
3309
|
+
/** The source's effective FLOOR CPM in dollars per thousand impressions (never cents) — the configured tag floor (`NUMERIC(8,2)`) or the partner's default, after any demand price action. It is an expectation used to order the waterfall, not a cleared price and not what the screen earns; `null` when the partner has no floor configured. */
|
|
2688
3310
|
cpm?: number | null;
|
|
2689
3311
|
/** High recent error rate measured server-side. Degraded sources are already pushed to the tail of the array for you — do NOT skip a source because of this flag. */
|
|
2690
3312
|
degraded?: boolean;
|
|
@@ -2692,7 +3314,9 @@ type VastWaterfallSource = {
|
|
|
2692
3314
|
duration_seconds?: number;
|
|
2693
3315
|
/** When false, skip this source entirely. Present-but-disabled sources may appear in the array. */
|
|
2694
3316
|
enabled?: boolean;
|
|
3317
|
+
/** Recent non-response rate for this demand source as a PERCENT 0-100 (not a 0-1 fraction). It is fleet-wide rather than per-screen, derived from an hourly rollup that can be ~15 minutes stale, and it is present only on sources that also carry `degraded: true`. Advisory: it never means skip the source. */
|
|
2695
3318
|
error_rate_pct?: number;
|
|
3319
|
+
/** No live producer — `null` on every source. The only assignment is guarded on a partner-level `fetchMode` setting that no demand partner in the registry defines. Do not branch on it. */
|
|
2696
3320
|
fetch_mode?: string | null;
|
|
2697
3321
|
/** Opaque public alias for the demand source. Echo it back as `variant_name` on `programmatic-event` reports. */
|
|
2698
3322
|
name?: string;
|
|
@@ -2702,6 +3326,7 @@ type VastWaterfallSource = {
|
|
|
2702
3326
|
request_id?: string;
|
|
2703
3327
|
/** Per-source budget in ms for fetching and loading THIS source. Distinct from the waterfall-wide `total_timeout_ms`. */
|
|
2704
3328
|
timeout_ms?: number;
|
|
3329
|
+
/** Fetch verbatim. The server has already appended this source's `request_id` and rewritten every partner-identifying path segment and query parameter to an opaque alias, so editing, re-ordering or re-signing the query breaks impression attribution. A `data:` URL can appear here for an inbound-DSP source that carries its VAST inline. */
|
|
2705
3330
|
vast_url?: string;
|
|
2706
3331
|
};
|
|
2707
3332
|
type Webhook = {
|
|
@@ -2758,24 +3383,40 @@ type WebhookEvent = {
|
|
|
2758
3383
|
timestamp?: string;
|
|
2759
3384
|
};
|
|
2760
3385
|
type WifiEnvironmentSnapshot = {
|
|
3386
|
+
/** Dimensionless 0.0-1.0: visible APs whose centre frequency is within ±10 MHz of the joined AP, divided by ALL visible APs in the capped list. Because the joined AP is itself in the numerator it is never 0 while connected — a literal `0.0` means not connected (or an empty scan), not an empty channel. Above ~0.5 indicates a dense venue, below ~0.2 an isolated one. */
|
|
2761
3387
|
channelCongestionRatio?: number | null;
|
|
3388
|
+
/** Channel width of the joined AP in MHz — but it is APPROXIMATE by construction: the producer returns the width of the FIRST entry in the scan list, which is only usually the joined AP, rather than matching on BSSID. `null` when not connected, on Android below API 23, or for an 80+80 MHz AP, which the width mapping does not cover. */
|
|
2762
3389
|
connectedChannelWidthMhz?: number | null;
|
|
3390
|
+
/** Centre frequency of the joined AP's primary 20 MHz channel, in MHz. `0` means not connected / unavailable, and that zero also zeroes `channelCongestionRatio` and forces `frequencyBand` to `unknown`. */
|
|
2763
3391
|
connectedFrequencyMhz?: number | null;
|
|
3392
|
+
/** Negotiated PHY link speed of the joined AP in Mbps (`WifiInfo.getLinkSpeed()`) — a link-rate ceiling, not measured throughput. `null` rather than 0 when not connected or when the platform reports a non-positive value. */
|
|
2764
3393
|
connectedLinkSpeedMbps?: number | null;
|
|
3394
|
+
/** RSSI of the JOINED access point in dBm. `-100` is a SENTINEL, not a measurement: it is the literal default when `WifiInfo` is unavailable or the device is not on Wi-Fi, so a genuinely weak link and no link at all are indistinguishable here. */
|
|
2765
3395
|
connectedSignalDbm?: number | null;
|
|
3396
|
+
/** Band of the JOINED AP: `2.4ghz`, `5ghz`, `6ghz` or `unknown`. It is derived from `connectedFrequencyMhz`, so a device that is not on Wi-Fi reports `unknown` rather than null — and the 5900-5924 MHz range falls into `unknown` too, because the classifier's 5 GHz and 6 GHz windows do not meet. */
|
|
2766
3397
|
frequencyBand?: string | null;
|
|
3398
|
+
/** Median RSSI in dBm across ALL visible APs (not just the joined one). It is the UPPER median for an even-sized list — the two middle values are not averaged — and falls back to the `-100` sentinel on an empty list. */
|
|
2767
3399
|
medianSignalDbm?: number | null;
|
|
3400
|
+
/** Distinct BSSIDs in the scan, after the 50-entry cap. It is literally the SAME value as `uniqueBssidCount` — both are assigned from one field — so the two never disagree and neither is a count of the uncapped scan. */
|
|
2768
3401
|
networkCount?: number | null;
|
|
3402
|
+
/** Population VARIANCE of the joined AP's RSSI in dBm SQUARED — not a standard deviation, and the divisor is N, not N-1. The window is the last 5 HEARTBEAT cycles held in a process-lifetime buffer, not the current scan, so it reads `0.0` on the first cycle after an agent restart; the `-100` not-connected sentinel is admitted into that history and skews it. Intended as a coarse motion indicator, not occupancy. */
|
|
2769
3403
|
rssiVariance?: number | null;
|
|
3404
|
+
/** UNIX EPOCH milliseconds (`System.currentTimeMillis()`) stamped when the scan COMPLETED — wall clock, not boot-relative, and not the scan start. It is a different clock from Android's own per-result `ScanResult.timestamp`, which is microseconds since boot and never reaches the wire. */
|
|
2770
3405
|
scanTimestampMs?: number | null;
|
|
3406
|
+
/** Strongest minus weakest visible RSSI, in dB — a positive difference, not an absolute level. It is `0` when fewer than two APs are visible, which is indistinguishable from a genuinely uniform RF environment. Used as a venue-openness hint. */
|
|
2771
3407
|
signalSpreadDbm?: number | null;
|
|
3408
|
+
/** Distinct BSSIDs after de-duplication and the 50-entry cap — the identical value to `networkCount` above, assigned from the same field, so it adds no independent information. */
|
|
2772
3409
|
uniqueBssidCount?: number | null;
|
|
2773
3410
|
[key: string]: unknown;
|
|
2774
3411
|
} | null;
|
|
2775
3412
|
type WifiScanResult = {
|
|
3413
|
+
/** Bonded channel width in MHz, mapped from Android's width enum to a literal 20, 40, 80 or 160. `null` on Android below API 23, on any read failure, AND for an 80+80 MHz AP — that variant has no mapping, so `null` does not mean narrow. */
|
|
2776
3414
|
channelWidthMhz?: number | null;
|
|
3415
|
+
/** Centre frequency of the AP's PRIMARY 20 MHz channel in MHz (2400-7200 accepted, covering 2.4/5/6 GHz) — not the centre of a wider bonded channel. Convert to a channel number yourself; the wire carries only the frequency. */
|
|
2777
3416
|
frequencyMhz: number;
|
|
3417
|
+
/** The AP's BSSID exactly as Android reports it, unhashed on the device (hashing happens server-side). Rows are de-duplicated by BSSID keeping the strongest reading, and the literal `02:00:00:00:00:00` — Android's redaction placeholder for a caller without location permission — is dropped rather than forwarded, so a permission-less device produces an empty list, not a list of placeholders. */
|
|
2778
3418
|
rawBssid: string;
|
|
3419
|
+
/** AP signal level in dBm (negative, bounded -120..0), from `ScanResult.level`. Note the wire key the Android agent actually sends is `signal_dbm`, not a snake_case form of this name — a mismatch of exactly this kind left the ClickHouse `rssi` column NULL on 100% of Wi-Fi rows until 2026-08-08. */
|
|
2779
3420
|
signalStrengthDbm: number;
|
|
2780
3421
|
[key: string]: unknown;
|
|
2781
3422
|
};
|